From 9270862642cdb751f3a3e30c81e628d5143a1d3c Mon Sep 17 00:00:00 2001 From: Prashant Date: Wed, 13 May 2026 20:40:58 +0530 Subject: [PATCH 1/7] timeout fix Signed-off-by: Prashant --- src/sap_cloud_sdk/agentgateway/_customer.py | 11 +++++++---- src/sap_cloud_sdk/agentgateway/_lob.py | 12 +++++++----- src/sap_cloud_sdk/agentgateway/agw_client.py | 17 ++++++++++++----- tests/agentgateway/unit/test_agw_client.py | 10 +++++----- 4 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/sap_cloud_sdk/agentgateway/_customer.py b/src/sap_cloud_sdk/agentgateway/_customer.py index a14ed7e..d083bf1 100644 --- a/src/sap_cloud_sdk/agentgateway/_customer.py +++ b/src/sap_cloud_sdk/agentgateway/_customer.py @@ -36,7 +36,7 @@ _CREDENTIALS_DEFAULT_PATH = "/etc/ums/credentials/credentials" # HTTP timeout for token requests and MCP server calls (seconds) -_HTTP_TIMEOUT = 30.0 +_HTTP_TIMEOUT = 60.0 # Resource URN for Agent Gateway token scope (hardcoded - production value) _AGW_RESOURCE_URN = "urn:sap:identity:application:provider:name:agent-gateway" @@ -371,6 +371,7 @@ async def _list_server_tools( url: str, auth_token: str, dependency: IntegrationDependency, + timeout: float = _HTTP_TIMEOUT, ) -> list[MCPTool]: """List tools from a single MCP server. @@ -390,7 +391,7 @@ async def _list_server_tools( "Authorization": f"Bearer {auth_token}", "x-correlation-id": str(uuid.uuid4()), }, - timeout=_HTTP_TIMEOUT, + timeout=timeout, ) as http_client: async with streamable_http_client(url, http_client=http_client) as ( read, @@ -428,6 +429,7 @@ async def _list_server_tools( async def get_mcp_tools_customer( credentials: CustomerCredentials, app_tid: str | None = None, + timeout: float = _HTTP_TIMEOUT, ) -> list[MCPTool]: """List all MCP tools from servers defined in credentials. @@ -471,7 +473,7 @@ async def get_mcp_tools_customer( ) try: - server_tools = await _list_server_tools(url, system_token, dep) + server_tools = await _list_server_tools(url, system_token, dep, timeout) tools.extend(server_tools) logger.debug("Loaded %d tool(s) from %s", len(server_tools), dep.ord_id) except Exception: @@ -488,6 +490,7 @@ async def call_mcp_tool_customer( tool: MCPTool, user_token: str | None, app_tid: str | None = None, + timeout: float = _HTTP_TIMEOUT, **kwargs, ) -> str: """Invoke an MCP tool using customer flow. @@ -532,7 +535,7 @@ async def call_mcp_tool_customer( "Authorization": f"Bearer {agw_token}", "x-correlation-id": str(uuid.uuid4()), }, - timeout=_HTTP_TIMEOUT, + timeout=timeout, ) as http_client: async with streamable_http_client(tool.url, http_client=http_client) as ( read, diff --git a/src/sap_cloud_sdk/agentgateway/_lob.py b/src/sap_cloud_sdk/agentgateway/_lob.py index 5533525..3241fe4 100644 --- a/src/sap_cloud_sdk/agentgateway/_lob.py +++ b/src/sap_cloud_sdk/agentgateway/_lob.py @@ -37,7 +37,7 @@ _DESTINATION_INSTANCE = "default" # HTTP timeout for MCP server requests (seconds) -_HTTP_TIMEOUT = 30.0 +_HTTP_TIMEOUT = 60.0 def _ias_dest_name() -> str: @@ -227,7 +227,7 @@ def _fetch_user_auth_sync(): async def list_server_tools( - dest_url: str, system_auth: str, fragment_name: str + dest_url: str, system_auth: str, fragment_name: str, timeout: float = _HTTP_TIMEOUT ) -> list[MCPTool]: """List tools from a single MCP server. @@ -241,7 +241,7 @@ async def list_server_tools( """ async with httpx.AsyncClient( headers={"Authorization": system_auth, "x-correlation-id": str(uuid.uuid4())}, - timeout=_HTTP_TIMEOUT, + timeout=timeout, ) as http_client: async with streamable_http_client(dest_url, http_client=http_client) as ( read, @@ -273,6 +273,7 @@ async def list_server_tools( async def get_mcp_tools_lob( tenant_subdomain: str, + timeout: float = _HTTP_TIMEOUT, ) -> list[MCPTool]: """List all MCP tools using LoB flow (destination-based). @@ -309,7 +310,7 @@ async def get_mcp_tools_lob( try: system_auth = await get_system_auth(tenant_subdomain) - server_tools = await list_server_tools(mcp_url, system_auth, fragment_name) + server_tools = await list_server_tools(mcp_url, system_auth, fragment_name, timeout) tools.extend(server_tools) logger.debug( "Loaded %d tool(s) from fragment '%s'", @@ -330,6 +331,7 @@ async def call_mcp_tool_lob( tool: MCPTool, user_token: str, tenant_subdomain: str, + timeout: float = _HTTP_TIMEOUT, **kwargs, ) -> str: """Invoke an MCP tool using LoB flow (destination-based). @@ -357,7 +359,7 @@ async def call_mcp_tool_lob( async with httpx.AsyncClient( headers={"Authorization": user_auth, "x-correlation-id": str(uuid.uuid4())}, - timeout=_HTTP_TIMEOUT, + timeout=timeout, ) as http_client: async with streamable_http_client(tool.url, http_client=http_client) as ( read, diff --git a/src/sap_cloud_sdk/agentgateway/agw_client.py b/src/sap_cloud_sdk/agentgateway/agw_client.py index f75d614..1abc260 100644 --- a/src/sap_cloud_sdk/agentgateway/agw_client.py +++ b/src/sap_cloud_sdk/agentgateway/agw_client.py @@ -72,6 +72,7 @@ class AgentGatewayClient: def __init__( self, tenant_subdomain: str | Callable[[], str] | None = None, + timeout: float = 60.0, ): """Initialize the Agent Gateway client. @@ -79,8 +80,11 @@ def __init__( tenant_subdomain: Tenant subdomain for multi-tenant lookup. Can be a string or a callable returning a string. Required for LoB agents, ignored for Customer agents. + timeout: HTTP timeout in seconds for token requests and MCP server calls. + Defaults to 60 seconds. """ self._tenant_subdomain = tenant_subdomain + self._timeout = timeout @staticmethod def _resolve_value( @@ -153,14 +157,14 @@ async def list_mcp_tools( "Customer agent credentials detected at '%s'", credentials_path ) credentials = load_customer_credentials(credentials_path) - return await get_mcp_tools_customer(credentials, app_tid) + return await get_mcp_tools_customer(credentials, app_tid, self._timeout) # LoB flow - requires tenant_subdomain if app_tid: logger.warning("app_tid parameter ignored for LoB agent flow") tenant = self._resolve_tenant_subdomain() - return await get_mcp_tools_lob(tenant) + return await get_mcp_tools_lob(tenant, self._timeout) except AgentGatewaySDKError: # Re-raise SDK errors as-is @@ -240,7 +244,7 @@ async def call_mcp_tool( credentials = load_customer_credentials(credentials_path) return await call_mcp_tool_customer( - credentials, tool, resolved_user_token, app_tid, **kwargs + credentials, tool, resolved_user_token, app_tid, self._timeout, **kwargs ) # LoB flow - requires user_token and tenant_subdomain @@ -253,7 +257,7 @@ async def call_mcp_tool( logger.warning("app_tid parameter ignored for LoB agent flow") tenant = self._resolve_tenant_subdomain() - return await call_mcp_tool_lob(tool, resolved_user_token, tenant, **kwargs) + return await call_mcp_tool_lob(tool, resolved_user_token, tenant, self._timeout, **kwargs) except AgentGatewaySDKError: # Re-raise SDK errors as-is @@ -267,6 +271,7 @@ async def call_mcp_tool( def create_client( tenant_subdomain: str | Callable[[], str] | None = None, + timeout: float = 60.0, ) -> AgentGatewayClient: """Create an Agent Gateway client for discovering and invoking MCP tools. @@ -277,6 +282,8 @@ def create_client( tenant_subdomain: Tenant subdomain for multi-tenant lookup. Can be a string or a callable returning a string. Required for LoB agents, ignored for Customer agents. + timeout: HTTP timeout in seconds for token requests and MCP server calls. + Defaults to 60 seconds. Returns: AgentGatewayClient instance. @@ -319,4 +326,4 @@ def create_client( ) ``` """ - return AgentGatewayClient(tenant_subdomain=tenant_subdomain) + return AgentGatewayClient(tenant_subdomain=tenant_subdomain, timeout=timeout) diff --git a/tests/agentgateway/unit/test_agw_client.py b/tests/agentgateway/unit/test_agw_client.py index 6a7bcc4..c60e8bb 100644 --- a/tests/agentgateway/unit/test_agw_client.py +++ b/tests/agentgateway/unit/test_agw_client.py @@ -165,7 +165,7 @@ async def test_with_callable_tenant(self): await agw_client.list_mcp_tools() - mock_lob.assert_called_once_with("my-tenant") + mock_lob.assert_called_once_with("my-tenant", 60.0) @pytest.mark.asyncio async def test_calls_lob_flow(self): @@ -185,7 +185,7 @@ async def test_calls_lob_flow(self): await agw_client.list_mcp_tools() - mock_lob.assert_called_once_with("my-tenant") + mock_lob.assert_called_once_with("my-tenant", 60.0) @pytest.mark.asyncio async def test_returns_tools_from_lob_flow(self): @@ -306,7 +306,7 @@ async def test_with_callable_user_token(self, mock_tool): assert result == "result" mock_lob.assert_called_once_with( - mock_tool, "my-jwt", "my-tenant", param1="value1" + mock_tool, "my-jwt", "my-tenant", 60.0, param1="value1" ) @pytest.mark.asyncio @@ -332,7 +332,7 @@ async def test_with_callable_tenant_subdomain(self, mock_tool): ) assert result == "result" - mock_lob.assert_called_once_with(mock_tool, "my-jwt", "my-tenant") + mock_lob.assert_called_once_with(mock_tool, "my-jwt", "my-tenant", 60.0) @pytest.mark.asyncio async def test_customer_credentials_calls_customer_flow(self, mock_tool): @@ -386,7 +386,7 @@ async def test_calls_lob_flow(self, mock_tool): assert result == "tool result" mock_lob.assert_called_once_with( - mock_tool, "jwt-token", "my-tenant", order_id="12345" + mock_tool, "jwt-token", "my-tenant", 60.0, order_id="12345" ) @pytest.mark.asyncio From 69a04b7d303d07d8d012ed950e7429b0efb4ade3 Mon Sep 17 00:00:00 2001 From: Prashant Date: Wed, 13 May 2026 21:20:39 +0530 Subject: [PATCH 2/7] improve error handling Signed-off-by: Prashant --- src/sap_cloud_sdk/agentgateway/agw_client.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/sap_cloud_sdk/agentgateway/agw_client.py b/src/sap_cloud_sdk/agentgateway/agw_client.py index 1abc260..5ea6bd5 100644 --- a/src/sap_cloud_sdk/agentgateway/agw_client.py +++ b/src/sap_cloud_sdk/agentgateway/agw_client.py @@ -171,7 +171,8 @@ async def list_mcp_tools( raise except Exception as e: logger.exception("Unexpected error during tool discovery") - raise AgentGatewaySDKError(f"Tool discovery failed: {e}") from e + cause = _unwrap_exception_group(e) + raise AgentGatewaySDKError(f"Tool discovery failed: {cause}") from e @record_metrics(Module.AGENTGATEWAY, Operation.AGENTGATEWAY_CALL_MCP_TOOL) async def call_mcp_tool( @@ -264,11 +265,19 @@ async def call_mcp_tool( raise except Exception as e: logger.exception("Unexpected error during tool invocation") + cause = _unwrap_exception_group(e) raise AgentGatewaySDKError( - f"Tool invocation failed for '{tool.name}': {e}" + f"Tool invocation failed for '{tool.name}': {cause}" ) from e +def _unwrap_exception_group(exc: Exception) -> Exception: + """Unwrap nested ExceptionGroups to present meaningful error messages.""" + while isinstance(exc, BaseExceptionGroup) and exc.exceptions: + exc = exc.exceptions[0] + return exc + + def create_client( tenant_subdomain: str | Callable[[], str] | None = None, timeout: float = 60.0, From be4b99632b3f1f5558623eff1d5aeef3fe014131 Mon Sep 17 00:00:00 2001 From: Prashant Date: Wed, 13 May 2026 21:47:53 +0530 Subject: [PATCH 3/7] formatting Signed-off-by: Prashant --- pyproject.toml | 2 +- src/sap_cloud_sdk/agentgateway/_lob.py | 4 +++- src/sap_cloud_sdk/agentgateway/agw_client.py | 11 +++++++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 32820f4..4a9bd6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sap-cloud-sdk" -version = "0.18.2" +version = "0.19.0" description = "SAP Cloud SDK for Python" readme = "README.md" license = "Apache-2.0" diff --git a/src/sap_cloud_sdk/agentgateway/_lob.py b/src/sap_cloud_sdk/agentgateway/_lob.py index 3241fe4..682399a 100644 --- a/src/sap_cloud_sdk/agentgateway/_lob.py +++ b/src/sap_cloud_sdk/agentgateway/_lob.py @@ -310,7 +310,9 @@ async def get_mcp_tools_lob( try: system_auth = await get_system_auth(tenant_subdomain) - server_tools = await list_server_tools(mcp_url, system_auth, fragment_name, timeout) + server_tools = await list_server_tools( + mcp_url, system_auth, fragment_name, timeout + ) tools.extend(server_tools) logger.debug( "Loaded %d tool(s) from fragment '%s'", diff --git a/src/sap_cloud_sdk/agentgateway/agw_client.py b/src/sap_cloud_sdk/agentgateway/agw_client.py index 5ea6bd5..140c7d6 100644 --- a/src/sap_cloud_sdk/agentgateway/agw_client.py +++ b/src/sap_cloud_sdk/agentgateway/agw_client.py @@ -245,7 +245,12 @@ async def call_mcp_tool( credentials = load_customer_credentials(credentials_path) return await call_mcp_tool_customer( - credentials, tool, resolved_user_token, app_tid, self._timeout, **kwargs + credentials, + tool, + resolved_user_token, + app_tid, + self._timeout, + **kwargs, ) # LoB flow - requires user_token and tenant_subdomain @@ -258,7 +263,9 @@ async def call_mcp_tool( logger.warning("app_tid parameter ignored for LoB agent flow") tenant = self._resolve_tenant_subdomain() - return await call_mcp_tool_lob(tool, resolved_user_token, tenant, self._timeout, **kwargs) + return await call_mcp_tool_lob( + tool, resolved_user_token, tenant, self._timeout, **kwargs + ) except AgentGatewaySDKError: # Re-raise SDK errors as-is From 660f204f4532e53ba62a11c0158d1ff31fe390db Mon Sep 17 00:00:00 2001 From: Prashant Date: Wed, 13 May 2026 21:51:04 +0530 Subject: [PATCH 4/7] ty issue Signed-off-by: Prashant --- src/sap_cloud_sdk/agentgateway/agw_client.py | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sap_cloud_sdk/agentgateway/agw_client.py b/src/sap_cloud_sdk/agentgateway/agw_client.py index 140c7d6..3e80d46 100644 --- a/src/sap_cloud_sdk/agentgateway/agw_client.py +++ b/src/sap_cloud_sdk/agentgateway/agw_client.py @@ -278,7 +278,7 @@ async def call_mcp_tool( ) from e -def _unwrap_exception_group(exc: Exception) -> Exception: +def _unwrap_exception_group(exc: BaseException) -> BaseException: """Unwrap nested ExceptionGroups to present meaningful error messages.""" while isinstance(exc, BaseExceptionGroup) and exc.exceptions: exc = exc.exceptions[0] diff --git a/uv.lock b/uv.lock index df51009..f65dd97 100644 --- a/uv.lock +++ b/uv.lock @@ -2924,7 +2924,7 @@ wheels = [ [[package]] name = "sap-cloud-sdk" -version = "0.18.2" +version = "0.19.0" source = { editable = "." } dependencies = [ { name = "grpcio" }, From e3d3c31f9cb3e3a97e22cf82ce03af65e4594ef0 Mon Sep 17 00:00:00 2001 From: Prashant Date: Wed, 13 May 2026 22:20:06 +0530 Subject: [PATCH 5/7] review comments Signed-off-by: Prashant --- src/sap_cloud_sdk/agentgateway/__init__.py | 3 +++ src/sap_cloud_sdk/agentgateway/agw_client.py | 25 ++++++++--------- src/sap_cloud_sdk/agentgateway/config.py | 15 +++++++++++ tests/agentgateway/unit/test_config.py | 28 ++++++++++++++++++++ 4 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 src/sap_cloud_sdk/agentgateway/config.py create mode 100644 tests/agentgateway/unit/test_config.py diff --git a/src/sap_cloud_sdk/agentgateway/__init__.py b/src/sap_cloud_sdk/agentgateway/__init__.py index e85fda5..eec21fe 100644 --- a/src/sap_cloud_sdk/agentgateway/__init__.py +++ b/src/sap_cloud_sdk/agentgateway/__init__.py @@ -53,6 +53,7 @@ """ from sap_cloud_sdk.agentgateway._models import MCPTool +from sap_cloud_sdk.agentgateway.config import ClientConfig from sap_cloud_sdk.agentgateway.agw_client import create_client, AgentGatewayClient from sap_cloud_sdk.agentgateway.exceptions import ( AgentGatewaySDKError, @@ -65,6 +66,8 @@ "create_client", # Client class "AgentGatewayClient", + # Configuration + "ClientConfig", # Data models "MCPTool", # Exceptions diff --git a/src/sap_cloud_sdk/agentgateway/agw_client.py b/src/sap_cloud_sdk/agentgateway/agw_client.py index 3e80d46..fc5653b 100644 --- a/src/sap_cloud_sdk/agentgateway/agw_client.py +++ b/src/sap_cloud_sdk/agentgateway/agw_client.py @@ -11,6 +11,7 @@ from typing import Callable from sap_cloud_sdk.agentgateway._models import MCPTool +from sap_cloud_sdk.agentgateway.config import ClientConfig from sap_cloud_sdk.agentgateway._customer import ( detect_customer_agent_credentials, load_customer_credentials, @@ -72,7 +73,7 @@ class AgentGatewayClient: def __init__( self, tenant_subdomain: str | Callable[[], str] | None = None, - timeout: float = 60.0, + config: ClientConfig | None = None, ): """Initialize the Agent Gateway client. @@ -80,11 +81,10 @@ def __init__( tenant_subdomain: Tenant subdomain for multi-tenant lookup. Can be a string or a callable returning a string. Required for LoB agents, ignored for Customer agents. - timeout: HTTP timeout in seconds for token requests and MCP server calls. - Defaults to 60 seconds. + config: Client configuration. Uses defaults if not provided. """ self._tenant_subdomain = tenant_subdomain - self._timeout = timeout + self._config = config or ClientConfig() @staticmethod def _resolve_value( @@ -157,14 +157,16 @@ async def list_mcp_tools( "Customer agent credentials detected at '%s'", credentials_path ) credentials = load_customer_credentials(credentials_path) - return await get_mcp_tools_customer(credentials, app_tid, self._timeout) + return await get_mcp_tools_customer( + credentials, app_tid, self._config.timeout + ) # LoB flow - requires tenant_subdomain if app_tid: logger.warning("app_tid parameter ignored for LoB agent flow") tenant = self._resolve_tenant_subdomain() - return await get_mcp_tools_lob(tenant, self._timeout) + return await get_mcp_tools_lob(tenant, self._config.timeout) except AgentGatewaySDKError: # Re-raise SDK errors as-is @@ -249,7 +251,7 @@ async def call_mcp_tool( tool, resolved_user_token, app_tid, - self._timeout, + self._config.timeout, **kwargs, ) @@ -264,7 +266,7 @@ async def call_mcp_tool( tenant = self._resolve_tenant_subdomain() return await call_mcp_tool_lob( - tool, resolved_user_token, tenant, self._timeout, **kwargs + tool, resolved_user_token, tenant, self._config.timeout, **kwargs ) except AgentGatewaySDKError: @@ -287,7 +289,7 @@ def _unwrap_exception_group(exc: BaseException) -> BaseException: def create_client( tenant_subdomain: str | Callable[[], str] | None = None, - timeout: float = 60.0, + config: ClientConfig | None = None, ) -> AgentGatewayClient: """Create an Agent Gateway client for discovering and invoking MCP tools. @@ -298,8 +300,7 @@ def create_client( tenant_subdomain: Tenant subdomain for multi-tenant lookup. Can be a string or a callable returning a string. Required for LoB agents, ignored for Customer agents. - timeout: HTTP timeout in seconds for token requests and MCP server calls. - Defaults to 60 seconds. + config: Client configuration. Uses defaults if not provided. Returns: AgentGatewayClient instance. @@ -342,4 +343,4 @@ def create_client( ) ``` """ - return AgentGatewayClient(tenant_subdomain=tenant_subdomain, timeout=timeout) + return AgentGatewayClient(tenant_subdomain=tenant_subdomain, config=config) diff --git a/src/sap_cloud_sdk/agentgateway/config.py b/src/sap_cloud_sdk/agentgateway/config.py new file mode 100644 index 0000000..b06aad4 --- /dev/null +++ b/src/sap_cloud_sdk/agentgateway/config.py @@ -0,0 +1,15 @@ +"""Configuration for Agent Gateway client.""" + +from dataclasses import dataclass + + +@dataclass +class ClientConfig: + """Configuration options for the Agent Gateway client. + + Attributes: + timeout: HTTP timeout in seconds for token requests and MCP server calls. + Defaults to 60 seconds. + """ + + timeout: float = 60.0 diff --git a/tests/agentgateway/unit/test_config.py b/tests/agentgateway/unit/test_config.py new file mode 100644 index 0000000..9499b3a --- /dev/null +++ b/tests/agentgateway/unit/test_config.py @@ -0,0 +1,28 @@ +"""Unit tests for ClientConfig.""" + +from sap_cloud_sdk.agentgateway import ClientConfig, create_client + + +class TestClientConfig: + """Tests for ClientConfig dataclass.""" + + def test_default_values(self): + """ClientConfig has sensible defaults.""" + config = ClientConfig() + assert config.timeout == 60.0 + + def test_custom_timeout(self): + """ClientConfig accepts custom timeout.""" + config = ClientConfig(timeout=120.0) + assert config.timeout == 120.0 + + def test_create_client_with_config(self): + """create_client accepts a ClientConfig.""" + config = ClientConfig(timeout=90.0) + client = create_client(config=config) + assert client._config.timeout == 90.0 + + def test_create_client_without_config_uses_defaults(self): + """create_client uses default config when none provided.""" + client = create_client() + assert client._config.timeout == 60.0 From f543795782a94590dfdb72066bb106a41ec74708 Mon Sep 17 00:00:00 2001 From: Prashant Date: Wed, 13 May 2026 22:56:15 +0530 Subject: [PATCH 6/7] review comments to address Signed-off-by: Prashant --- src/sap_cloud_sdk/agentgateway/_customer.py | 24 ++++++++++++-------- src/sap_cloud_sdk/agentgateway/_lob.py | 9 +++----- src/sap_cloud_sdk/agentgateway/agw_client.py | 4 ++-- tests/agentgateway/unit/test_customer.py | 24 +++++++++++--------- tests/agentgateway/unit/test_lob.py | 12 +++++----- 5 files changed, 38 insertions(+), 35 deletions(-) diff --git a/src/sap_cloud_sdk/agentgateway/_customer.py b/src/sap_cloud_sdk/agentgateway/_customer.py index d083bf1..0f6ffb4 100644 --- a/src/sap_cloud_sdk/agentgateway/_customer.py +++ b/src/sap_cloud_sdk/agentgateway/_customer.py @@ -35,9 +35,6 @@ # Default credential path for Kyma production deployments _CREDENTIALS_DEFAULT_PATH = "/etc/ums/credentials/credentials" -# HTTP timeout for token requests and MCP server calls (seconds) -_HTTP_TIMEOUT = 60.0 - # Resource URN for Agent Gateway token scope (hardcoded - production value) _AGW_RESOURCE_URN = "urn:sap:identity:application:provider:name:agent-gateway" @@ -213,6 +210,7 @@ def _create_ssl_context(certificate: str, private_key: str) -> ssl.SSLContext: def _request_token_mtls( credentials: CustomerCredentials, grant_type: str, + timeout: float, app_tid: str | None = None, extra_data: dict | None = None, ) -> str: @@ -255,7 +253,7 @@ def _request_token_mtls( try: with httpx.Client( verify=ssl_context, - timeout=_HTTP_TIMEOUT, + timeout=timeout, ) as client: response = client.post( credentials.token_service_url, @@ -293,6 +291,7 @@ def _request_token_mtls( def get_system_token_mtls( credentials: CustomerCredentials, + timeout: float, app_tid: str | None = None, ) -> str: """Get system-scoped token using mTLS client credentials flow. @@ -301,6 +300,7 @@ def get_system_token_mtls( Args: credentials: Customer credentials. + timeout: HTTP timeout in seconds. app_tid: BTP Application Tenant ID of subscriber (optional). Returns: @@ -310,6 +310,7 @@ def get_system_token_mtls( return _request_token_mtls( credentials, grant_type=_GRANT_TYPE_CLIENT_CREDENTIALS, + timeout=timeout, app_tid=app_tid, extra_data={"response_type": "token"}, ) @@ -318,6 +319,7 @@ def get_system_token_mtls( def exchange_user_token( credentials: CustomerCredentials, user_token: str, + timeout: float, app_tid: str | None = None, ) -> str: """Exchange user token for AGW-scoped token using jwt-bearer grant. @@ -328,6 +330,7 @@ def exchange_user_token( Args: credentials: Customer credentials. user_token: User's JWT token to exchange. + timeout: HTTP timeout in seconds. app_tid: BTP Application Tenant ID of subscriber (optional). Returns: @@ -337,6 +340,7 @@ def exchange_user_token( return _request_token_mtls( credentials, grant_type=_GRANT_TYPE_JWT_BEARER, + timeout=timeout, app_tid=app_tid, extra_data={ "assertion": user_token, @@ -371,7 +375,7 @@ async def _list_server_tools( url: str, auth_token: str, dependency: IntegrationDependency, - timeout: float = _HTTP_TIMEOUT, + timeout: float, ) -> list[MCPTool]: """List tools from a single MCP server. @@ -428,8 +432,8 @@ async def _list_server_tools( async def get_mcp_tools_customer( credentials: CustomerCredentials, + timeout: float, app_tid: str | None = None, - timeout: float = _HTTP_TIMEOUT, ) -> list[MCPTool]: """List all MCP tools from servers defined in credentials. @@ -458,7 +462,7 @@ async def get_mcp_tools_customer( # Get system token for discovery loop = asyncio.get_running_loop() system_token = await loop.run_in_executor( - None, get_system_token_mtls, credentials, app_tid + None, get_system_token_mtls, credentials, timeout, app_tid ) tools: list[MCPTool] = [] @@ -489,8 +493,8 @@ async def call_mcp_tool_customer( credentials: CustomerCredentials, tool: MCPTool, user_token: str | None, + timeout: float, app_tid: str | None = None, - timeout: float = _HTTP_TIMEOUT, **kwargs, ) -> str: """Invoke an MCP tool using customer flow. @@ -516,7 +520,7 @@ async def call_mcp_tool_customer( if user_token: # Exchange user token for AGW-scoped token (with principal propagation) agw_token = await loop.run_in_executor( - None, exchange_user_token, credentials, user_token, app_tid + None, exchange_user_token, credentials, user_token, timeout, app_tid ) else: # TODO: IBD workaround - use system token when user_token is not available. @@ -527,7 +531,7 @@ async def call_mcp_tool_customer( "Principal propagation will NOT work." ) agw_token = await loop.run_in_executor( - None, get_system_token_mtls, credentials, app_tid + None, get_system_token_mtls, credentials, timeout, app_tid ) async with httpx.AsyncClient( diff --git a/src/sap_cloud_sdk/agentgateway/_lob.py b/src/sap_cloud_sdk/agentgateway/_lob.py index 682399a..15c9707 100644 --- a/src/sap_cloud_sdk/agentgateway/_lob.py +++ b/src/sap_cloud_sdk/agentgateway/_lob.py @@ -36,9 +36,6 @@ _DESTINATION_INSTANCE = "default" -# HTTP timeout for MCP server requests (seconds) -_HTTP_TIMEOUT = 60.0 - def _ias_dest_name() -> str: """Get IAS destination name based on landscape. @@ -227,7 +224,7 @@ def _fetch_user_auth_sync(): async def list_server_tools( - dest_url: str, system_auth: str, fragment_name: str, timeout: float = _HTTP_TIMEOUT + dest_url: str, system_auth: str, fragment_name: str, timeout: float ) -> list[MCPTool]: """List tools from a single MCP server. @@ -273,7 +270,7 @@ async def list_server_tools( async def get_mcp_tools_lob( tenant_subdomain: str, - timeout: float = _HTTP_TIMEOUT, + timeout: float, ) -> list[MCPTool]: """List all MCP tools using LoB flow (destination-based). @@ -333,7 +330,7 @@ async def call_mcp_tool_lob( tool: MCPTool, user_token: str, tenant_subdomain: str, - timeout: float = _HTTP_TIMEOUT, + timeout: float, **kwargs, ) -> str: """Invoke an MCP tool using LoB flow (destination-based). diff --git a/src/sap_cloud_sdk/agentgateway/agw_client.py b/src/sap_cloud_sdk/agentgateway/agw_client.py index fc5653b..f897b70 100644 --- a/src/sap_cloud_sdk/agentgateway/agw_client.py +++ b/src/sap_cloud_sdk/agentgateway/agw_client.py @@ -158,7 +158,7 @@ async def list_mcp_tools( ) credentials = load_customer_credentials(credentials_path) return await get_mcp_tools_customer( - credentials, app_tid, self._config.timeout + credentials, self._config.timeout, app_tid ) # LoB flow - requires tenant_subdomain @@ -250,8 +250,8 @@ async def call_mcp_tool( credentials, tool, resolved_user_token, - app_tid, self._config.timeout, + app_tid, **kwargs, ) diff --git a/tests/agentgateway/unit/test_customer.py b/tests/agentgateway/unit/test_customer.py index 9e1f7cf..4ed170b 100644 --- a/tests/agentgateway/unit/test_customer.py +++ b/tests/agentgateway/unit/test_customer.py @@ -305,7 +305,7 @@ def test_requests_client_credentials_token(self, credentials): mock_client.post.return_value = mock_response mock_client_class.return_value = mock_client - result = get_system_token_mtls(credentials) + result = get_system_token_mtls(credentials, timeout=60.0) assert result == "system-token-123" mock_client.post.assert_called_once() @@ -332,7 +332,7 @@ def test_raises_on_failed_request(self, credentials): mock_client_class.return_value = mock_client with pytest.raises(AgentGatewaySDKError, match="Token request failed"): - get_system_token_mtls(credentials) + get_system_token_mtls(credentials, timeout=60.0) # ============================================================ @@ -374,7 +374,7 @@ def test_exchanges_user_token_with_jwt_bearer(self, credentials): mock_client.post.return_value = mock_response mock_client_class.return_value = mock_client - result = exchange_user_token(credentials, "user-jwt-token") + result = exchange_user_token(credentials, "user-jwt-token", timeout=60.0) assert result == "exchanged-token-123" call_args = mock_client.post.call_args @@ -402,7 +402,9 @@ def test_passes_app_tid_when_provided(self, credentials): mock_client.post.return_value = mock_response mock_client_class.return_value = mock_client - result = exchange_user_token(credentials, "user-jwt", app_tid="test-tid") + result = exchange_user_token( + credentials, "user-jwt", timeout=60.0, app_tid="test-tid" + ) assert result == "token-with-tid" call_args = mock_client.post.call_args @@ -449,7 +451,7 @@ async def test_raises_when_empty_dependencies(self): with pytest.raises( AgentGatewaySDKError, match="integrationDependencies is empty" ): - await get_mcp_tools_customer(credentials) + await get_mcp_tools_customer(credentials, timeout=60.0) @pytest.mark.asyncio async def test_discovers_tools_from_credentials(self, credentials): @@ -475,7 +477,7 @@ async def test_discovers_tools_from_credentials(self, credentials): return_value=mock_tools, ) as mock_list, ): - result = await get_mcp_tools_customer(credentials) + result = await get_mcp_tools_customer(credentials, timeout=60.0) assert len(result) == 1 assert result[0].name == "list_cost_centers" @@ -523,7 +525,7 @@ async def mock_list_tools(*args, **kwargs): side_effect=mock_list_tools, ), ): - result = await get_mcp_tools_customer(credentials) + result = await get_mcp_tools_customer(credentials, timeout=60.0) # Should still return tools from server2 assert len(result) == 1 @@ -613,11 +615,11 @@ async def test_exchanges_user_token_before_call(self, credentials, mock_tool): mock_session_class.return_value = mock_session_ctx result = await call_mcp_tool_customer( - credentials, mock_tool, "user-jwt", order_id="12345" + credentials, mock_tool, "user-jwt", 60.0, order_id="12345" ) assert result == "Order created successfully" - mock_exchange.assert_called_once_with(credentials, "user-jwt", None) + mock_exchange.assert_called_once_with(credentials, "user-jwt", 60.0, None) @pytest.mark.asyncio async def test_uses_system_token_when_user_token_not_provided( @@ -669,10 +671,10 @@ async def test_uses_system_token_when_user_token_not_provided( # Call without user_token (None) result = await call_mcp_tool_customer( - credentials, mock_tool, None, order_id="12345" + credentials, mock_tool, None, 60.0, order_id="12345" ) assert result == "Result with system token" # Should use system token, not exchange - mock_system_token.assert_called_once_with(credentials, None) + mock_system_token.assert_called_once_with(credentials, 60.0, None) mock_exchange.assert_not_called() diff --git a/tests/agentgateway/unit/test_lob.py b/tests/agentgateway/unit/test_lob.py index 4f9a6f3..35b5e05 100644 --- a/tests/agentgateway/unit/test_lob.py +++ b/tests/agentgateway/unit/test_lob.py @@ -323,7 +323,7 @@ async def test_returns_empty_when_no_fragments(self): with patch("sap_cloud_sdk.agentgateway._lob.list_mcp_fragments") as mock_list: mock_list.return_value = [] - result = await get_mcp_tools_lob("tenant-sub") + result = await get_mcp_tools_lob("tenant-sub", 60.0) assert result == [] @@ -337,7 +337,7 @@ async def test_skips_fragments_without_url(self): with patch("sap_cloud_sdk.agentgateway._lob.list_mcp_fragments") as mock_list: mock_list.return_value = [fragment] - result = await get_mcp_tools_lob("tenant-sub") + result = await get_mcp_tools_lob("tenant-sub", 60.0) assert result == [] @@ -372,7 +372,7 @@ async def test_uses_fragment_name_directly(self): mock_auth.return_value = "Bearer token" mock_tools.return_value = [mock_tool] - await get_mcp_tools_lob("tenant-sub") + await get_mcp_tools_lob("tenant-sub", 60.0) # Verify get_system_auth called with just tenant_subdomain mock_auth.assert_called_once_with("tenant-sub") @@ -418,7 +418,7 @@ async def test_handles_exception_for_single_fragment(self): mock_auth.side_effect = [Exception("Auth failed"), "Bearer token"] mock_tools.return_value = [mock_tool] - result = await get_mcp_tools_lob("tenant-sub") + result = await get_mcp_tools_lob("tenant-sub", 60.0) # Should still get tools from second fragment assert len(result) == 1 @@ -477,7 +477,7 @@ async def test_calls_tool_with_user_auth(self): mock_session.return_value.__aenter__.return_value = mock_session_instance result = await call_mcp_tool_lob( - tool, "user-jwt", "tenant-sub", param1="value1" + tool, "user-jwt", "tenant-sub", 60.0, param1="value1" ) assert result == "Tool result" @@ -527,6 +527,6 @@ async def test_returns_empty_string_when_no_content(self): mock_session_instance.call_tool = AsyncMock(return_value=mock_result) mock_session.return_value.__aenter__.return_value = mock_session_instance - result = await call_mcp_tool_lob(tool, "user-jwt", "tenant-sub") + result = await call_mcp_tool_lob(tool, "user-jwt", "tenant-sub", 60.0) assert result == "" From 8a8408a1986142c971e97d749b9fcee31bf5a022 Mon Sep 17 00:00:00 2001 From: Prashant Date: Thu, 14 May 2026 00:25:33 +0530 Subject: [PATCH 7/7] use const Signed-off-by: Prashant --- src/sap_cloud_sdk/agentgateway/config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/sap_cloud_sdk/agentgateway/config.py b/src/sap_cloud_sdk/agentgateway/config.py index b06aad4..427f96b 100644 --- a/src/sap_cloud_sdk/agentgateway/config.py +++ b/src/sap_cloud_sdk/agentgateway/config.py @@ -2,6 +2,8 @@ from dataclasses import dataclass +DEFAULT_TIMEOUT_SECONDS = 60.0 + @dataclass class ClientConfig: @@ -12,4 +14,4 @@ class ClientConfig: Defaults to 60 seconds. """ - timeout: float = 60.0 + timeout: float = DEFAULT_TIMEOUT_SECONDS