Skip to content

Commit 1c2e930

Browse files
feat(agentgateway): make timeout configurable and increase default timeout value (#118)
Signed-off-by: Prashant <prashant.rakheja@sap.com> Co-authored-by: Nicole Gomes <47161082+NicoleMGomes@users.noreply.github.com>
1 parent 46744ee commit 1c2e930

11 files changed

Lines changed: 130 additions & 48 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "sap-cloud-sdk"
3-
version = "0.18.4"
3+
version = "0.19.0"
44
description = "SAP Cloud SDK for Python"
55
readme = "README.md"
66
license = "Apache-2.0"

src/sap_cloud_sdk/agentgateway/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"""
5454

5555
from sap_cloud_sdk.agentgateway._models import MCPTool
56+
from sap_cloud_sdk.agentgateway.config import ClientConfig
5657
from sap_cloud_sdk.agentgateway.agw_client import create_client, AgentGatewayClient
5758
from sap_cloud_sdk.agentgateway.exceptions import (
5859
AgentGatewaySDKError,
@@ -65,6 +66,8 @@
6566
"create_client",
6667
# Client class
6768
"AgentGatewayClient",
69+
# Configuration
70+
"ClientConfig",
6871
# Data models
6972
"MCPTool",
7073
# Exceptions

src/sap_cloud_sdk/agentgateway/_customer.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,6 @@
3535
# Default credential path for Kyma production deployments
3636
_CREDENTIALS_DEFAULT_PATH = "/etc/ums/credentials/credentials"
3737

38-
# HTTP timeout for token requests and MCP server calls (seconds)
39-
_HTTP_TIMEOUT = 30.0
40-
4138
# Resource URN for Agent Gateway token scope (hardcoded - production value)
4239
_AGW_RESOURCE_URN = "urn:sap:identity:application:provider:name:agent-gateway"
4340

@@ -213,6 +210,7 @@ def _create_ssl_context(certificate: str, private_key: str) -> ssl.SSLContext:
213210
def _request_token_mtls(
214211
credentials: CustomerCredentials,
215212
grant_type: str,
213+
timeout: float,
216214
app_tid: str | None = None,
217215
extra_data: dict | None = None,
218216
) -> str:
@@ -255,7 +253,7 @@ def _request_token_mtls(
255253
try:
256254
with httpx.Client(
257255
verify=ssl_context,
258-
timeout=_HTTP_TIMEOUT,
256+
timeout=timeout,
259257
) as client:
260258
response = client.post(
261259
credentials.token_service_url,
@@ -293,6 +291,7 @@ def _request_token_mtls(
293291

294292
def get_system_token_mtls(
295293
credentials: CustomerCredentials,
294+
timeout: float,
296295
app_tid: str | None = None,
297296
) -> str:
298297
"""Get system-scoped token using mTLS client credentials flow.
@@ -301,6 +300,7 @@ def get_system_token_mtls(
301300
302301
Args:
303302
credentials: Customer credentials.
303+
timeout: HTTP timeout in seconds.
304304
app_tid: BTP Application Tenant ID of subscriber (optional).
305305
306306
Returns:
@@ -310,6 +310,7 @@ def get_system_token_mtls(
310310
return _request_token_mtls(
311311
credentials,
312312
grant_type=_GRANT_TYPE_CLIENT_CREDENTIALS,
313+
timeout=timeout,
313314
app_tid=app_tid,
314315
extra_data={"response_type": "token"},
315316
)
@@ -318,6 +319,7 @@ def get_system_token_mtls(
318319
def exchange_user_token(
319320
credentials: CustomerCredentials,
320321
user_token: str,
322+
timeout: float,
321323
app_tid: str | None = None,
322324
) -> str:
323325
"""Exchange user token for AGW-scoped token using jwt-bearer grant.
@@ -328,6 +330,7 @@ def exchange_user_token(
328330
Args:
329331
credentials: Customer credentials.
330332
user_token: User's JWT token to exchange.
333+
timeout: HTTP timeout in seconds.
331334
app_tid: BTP Application Tenant ID of subscriber (optional).
332335
333336
Returns:
@@ -337,6 +340,7 @@ def exchange_user_token(
337340
return _request_token_mtls(
338341
credentials,
339342
grant_type=_GRANT_TYPE_JWT_BEARER,
343+
timeout=timeout,
340344
app_tid=app_tid,
341345
extra_data={
342346
"assertion": user_token,
@@ -371,6 +375,7 @@ async def _list_server_tools(
371375
url: str,
372376
auth_token: str,
373377
dependency: IntegrationDependency,
378+
timeout: float,
374379
) -> list[MCPTool]:
375380
"""List tools from a single MCP server.
376381
@@ -390,7 +395,7 @@ async def _list_server_tools(
390395
"Authorization": f"Bearer {auth_token}",
391396
"x-correlation-id": str(uuid.uuid4()),
392397
},
393-
timeout=_HTTP_TIMEOUT,
398+
timeout=timeout,
394399
) as http_client:
395400
async with streamable_http_client(url, http_client=http_client) as (
396401
read,
@@ -427,6 +432,7 @@ async def _list_server_tools(
427432

428433
async def get_mcp_tools_customer(
429434
credentials: CustomerCredentials,
435+
timeout: float,
430436
app_tid: str | None = None,
431437
) -> list[MCPTool]:
432438
"""List all MCP tools from servers defined in credentials.
@@ -456,7 +462,7 @@ async def get_mcp_tools_customer(
456462
# Get system token for discovery
457463
loop = asyncio.get_running_loop()
458464
system_token = await loop.run_in_executor(
459-
None, get_system_token_mtls, credentials, app_tid
465+
None, get_system_token_mtls, credentials, timeout, app_tid
460466
)
461467

462468
tools: list[MCPTool] = []
@@ -471,7 +477,7 @@ async def get_mcp_tools_customer(
471477
)
472478

473479
try:
474-
server_tools = await _list_server_tools(url, system_token, dep)
480+
server_tools = await _list_server_tools(url, system_token, dep, timeout)
475481
tools.extend(server_tools)
476482
logger.debug("Loaded %d tool(s) from %s", len(server_tools), dep.ord_id)
477483
except Exception:
@@ -487,6 +493,7 @@ async def call_mcp_tool_customer(
487493
credentials: CustomerCredentials,
488494
tool: MCPTool,
489495
user_token: str | None,
496+
timeout: float,
490497
app_tid: str | None = None,
491498
**kwargs,
492499
) -> str:
@@ -513,7 +520,7 @@ async def call_mcp_tool_customer(
513520
if user_token:
514521
# Exchange user token for AGW-scoped token (with principal propagation)
515522
agw_token = await loop.run_in_executor(
516-
None, exchange_user_token, credentials, user_token, app_tid
523+
None, exchange_user_token, credentials, user_token, timeout, app_tid
517524
)
518525
else:
519526
# TODO: IBD workaround - use system token when user_token is not available.
@@ -524,15 +531,15 @@ async def call_mcp_tool_customer(
524531
"Principal propagation will NOT work."
525532
)
526533
agw_token = await loop.run_in_executor(
527-
None, get_system_token_mtls, credentials, app_tid
534+
None, get_system_token_mtls, credentials, timeout, app_tid
528535
)
529536

530537
async with httpx.AsyncClient(
531538
headers={
532539
"Authorization": f"Bearer {agw_token}",
533540
"x-correlation-id": str(uuid.uuid4()),
534541
},
535-
timeout=_HTTP_TIMEOUT,
542+
timeout=timeout,
536543
) as http_client:
537544
async with streamable_http_client(tool.url, http_client=http_client) as (
538545
read,

src/sap_cloud_sdk/agentgateway/_lob.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,6 @@
3636

3737
_DESTINATION_INSTANCE = "default"
3838

39-
# HTTP timeout for MCP server requests (seconds)
40-
_HTTP_TIMEOUT = 30.0
41-
4239

4340
def _ias_dest_name() -> str:
4441
"""Get IAS destination name based on landscape.
@@ -227,7 +224,7 @@ def _fetch_user_auth_sync():
227224

228225

229226
async def list_server_tools(
230-
dest_url: str, system_auth: str, fragment_name: str
227+
dest_url: str, system_auth: str, fragment_name: str, timeout: float
231228
) -> list[MCPTool]:
232229
"""List tools from a single MCP server.
233230
@@ -241,7 +238,7 @@ async def list_server_tools(
241238
"""
242239
async with httpx.AsyncClient(
243240
headers={"Authorization": system_auth, "x-correlation-id": str(uuid.uuid4())},
244-
timeout=_HTTP_TIMEOUT,
241+
timeout=timeout,
245242
) as http_client:
246243
async with streamable_http_client(dest_url, http_client=http_client) as (
247244
read,
@@ -273,6 +270,7 @@ async def list_server_tools(
273270

274271
async def get_mcp_tools_lob(
275272
tenant_subdomain: str,
273+
timeout: float,
276274
) -> list[MCPTool]:
277275
"""List all MCP tools using LoB flow (destination-based).
278276
@@ -309,7 +307,9 @@ async def get_mcp_tools_lob(
309307

310308
try:
311309
system_auth = await get_system_auth(tenant_subdomain)
312-
server_tools = await list_server_tools(mcp_url, system_auth, fragment_name)
310+
server_tools = await list_server_tools(
311+
mcp_url, system_auth, fragment_name, timeout
312+
)
313313
tools.extend(server_tools)
314314
logger.debug(
315315
"Loaded %d tool(s) from fragment '%s'",
@@ -330,6 +330,7 @@ async def call_mcp_tool_lob(
330330
tool: MCPTool,
331331
user_token: str,
332332
tenant_subdomain: str,
333+
timeout: float,
333334
**kwargs,
334335
) -> str:
335336
"""Invoke an MCP tool using LoB flow (destination-based).
@@ -357,7 +358,7 @@ async def call_mcp_tool_lob(
357358

358359
async with httpx.AsyncClient(
359360
headers={"Authorization": user_auth, "x-correlation-id": str(uuid.uuid4())},
360-
timeout=_HTTP_TIMEOUT,
361+
timeout=timeout,
361362
) as http_client:
362363
async with streamable_http_client(tool.url, http_client=http_client) as (
363364
read,

src/sap_cloud_sdk/agentgateway/agw_client.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from typing import Callable
1212

1313
from sap_cloud_sdk.agentgateway._models import MCPTool
14+
from sap_cloud_sdk.agentgateway.config import ClientConfig
1415
from sap_cloud_sdk.agentgateway._customer import (
1516
detect_customer_agent_credentials,
1617
load_customer_credentials,
@@ -72,15 +73,18 @@ class AgentGatewayClient:
7273
def __init__(
7374
self,
7475
tenant_subdomain: str | Callable[[], str] | None = None,
76+
config: ClientConfig | None = None,
7577
):
7678
"""Initialize the Agent Gateway client.
7779
7880
Args:
7981
tenant_subdomain: Tenant subdomain for multi-tenant lookup.
8082
Can be a string or a callable returning a string.
8183
Required for LoB agents, ignored for Customer agents.
84+
config: Client configuration. Uses defaults if not provided.
8285
"""
8386
self._tenant_subdomain = tenant_subdomain
87+
self._config = config or ClientConfig()
8488

8589
@staticmethod
8690
def _resolve_value(
@@ -153,21 +157,24 @@ async def list_mcp_tools(
153157
"Customer agent credentials detected at '%s'", credentials_path
154158
)
155159
credentials = load_customer_credentials(credentials_path)
156-
return await get_mcp_tools_customer(credentials, app_tid)
160+
return await get_mcp_tools_customer(
161+
credentials, self._config.timeout, app_tid
162+
)
157163

158164
# LoB flow - requires tenant_subdomain
159165
if app_tid:
160166
logger.warning("app_tid parameter ignored for LoB agent flow")
161167

162168
tenant = self._resolve_tenant_subdomain()
163-
return await get_mcp_tools_lob(tenant)
169+
return await get_mcp_tools_lob(tenant, self._config.timeout)
164170

165171
except AgentGatewaySDKError:
166172
# Re-raise SDK errors as-is
167173
raise
168174
except Exception as e:
169175
logger.exception("Unexpected error during tool discovery")
170-
raise AgentGatewaySDKError(f"Tool discovery failed: {e}") from e
176+
cause = _unwrap_exception_group(e)
177+
raise AgentGatewaySDKError(f"Tool discovery failed: {cause}") from e
171178

172179
@record_metrics(Module.AGENTGATEWAY, Operation.AGENTGATEWAY_CALL_MCP_TOOL)
173180
async def call_mcp_tool(
@@ -240,7 +247,12 @@ async def call_mcp_tool(
240247

241248
credentials = load_customer_credentials(credentials_path)
242249
return await call_mcp_tool_customer(
243-
credentials, tool, resolved_user_token, app_tid, **kwargs
250+
credentials,
251+
tool,
252+
resolved_user_token,
253+
self._config.timeout,
254+
app_tid,
255+
**kwargs,
244256
)
245257

246258
# LoB flow - requires user_token and tenant_subdomain
@@ -253,20 +265,31 @@ async def call_mcp_tool(
253265
logger.warning("app_tid parameter ignored for LoB agent flow")
254266

255267
tenant = self._resolve_tenant_subdomain()
256-
return await call_mcp_tool_lob(tool, resolved_user_token, tenant, **kwargs)
268+
return await call_mcp_tool_lob(
269+
tool, resolved_user_token, tenant, self._config.timeout, **kwargs
270+
)
257271

258272
except AgentGatewaySDKError:
259273
# Re-raise SDK errors as-is
260274
raise
261275
except Exception as e:
262276
logger.exception("Unexpected error during tool invocation")
277+
cause = _unwrap_exception_group(e)
263278
raise AgentGatewaySDKError(
264-
f"Tool invocation failed for '{tool.name}': {e}"
279+
f"Tool invocation failed for '{tool.name}': {cause}"
265280
) from e
266281

267282

283+
def _unwrap_exception_group(exc: BaseException) -> BaseException:
284+
"""Unwrap nested ExceptionGroups to present meaningful error messages."""
285+
while isinstance(exc, BaseExceptionGroup) and exc.exceptions:
286+
exc = exc.exceptions[0]
287+
return exc
288+
289+
268290
def create_client(
269291
tenant_subdomain: str | Callable[[], str] | None = None,
292+
config: ClientConfig | None = None,
270293
) -> AgentGatewayClient:
271294
"""Create an Agent Gateway client for discovering and invoking MCP tools.
272295
@@ -277,6 +300,7 @@ def create_client(
277300
tenant_subdomain: Tenant subdomain for multi-tenant lookup.
278301
Can be a string or a callable returning a string.
279302
Required for LoB agents, ignored for Customer agents.
303+
config: Client configuration. Uses defaults if not provided.
280304
281305
Returns:
282306
AgentGatewayClient instance.
@@ -319,4 +343,4 @@ def create_client(
319343
)
320344
```
321345
"""
322-
return AgentGatewayClient(tenant_subdomain=tenant_subdomain)
346+
return AgentGatewayClient(tenant_subdomain=tenant_subdomain, config=config)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""Configuration for Agent Gateway client."""
2+
3+
from dataclasses import dataclass
4+
5+
DEFAULT_TIMEOUT_SECONDS = 60.0
6+
7+
8+
@dataclass
9+
class ClientConfig:
10+
"""Configuration options for the Agent Gateway client.
11+
12+
Attributes:
13+
timeout: HTTP timeout in seconds for token requests and MCP server calls.
14+
Defaults to 60 seconds.
15+
"""
16+
17+
timeout: float = DEFAULT_TIMEOUT_SECONDS

0 commit comments

Comments
 (0)