Skip to content

Commit d513d5c

Browse files
committed
chore: merge main (0.46.1) and bump version to 0.47.0
2 parents 184bf32 + c73762a commit d513d5c

13 files changed

Lines changed: 291 additions & 30 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.46.0"
3+
version = "0.47.0"
44
description = "SAP Cloud SDK for Python"
55
readme = "README.md"
66
license = "Apache-2.0"

‎src/sap_cloud_sdk/agentgateway/_customer.py‎

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@
2323
import httpx
2424
from mcp import ClientSession
2525
from mcp.client.streamable_http import streamable_http_client
26+
from mcp.shared.exceptions import McpError
2627

2728
from sap_cloud_sdk.agentgateway._dependencies_resolver import (
2829
EnvironmentDependenciesResolver,
2930
IntegrationDependenciesResolver,
3031
)
3132
from sap_cloud_sdk.agentgateway._models import (
33+
JsonRpcError,
3234
CustomerCredentials,
3335
IntegrationDependency,
3436
MCPTool,
@@ -680,15 +682,42 @@ def _log_mcp_server_error(ord_id: str, exc: BaseException) -> None:
680682
_log_mcp_server_error(ord_id, inner)
681683
return
682684
if isinstance(exc, httpx.HTTPStatusError):
685+
try:
686+
body = exc.response.text
687+
except httpx.ResponseNotRead:
688+
body = None
689+
rpc_error = JsonRpcError.parse(body) if body else None
690+
if rpc_error:
691+
logger.error(
692+
"Failed to load tools from %s — %s returned HTTP %d [JSON-RPC %d]: %s",
693+
ord_id,
694+
exc.request.url,
695+
exc.response.status_code,
696+
rpc_error.code,
697+
rpc_error.message,
698+
)
699+
else:
700+
logger.error(
701+
"Failed to load tools from %s — %s returned HTTP %d: %s",
702+
ord_id,
703+
exc.request.url,
704+
exc.response.status_code,
705+
body[:500] if body else "(response body not available)",
706+
)
707+
elif isinstance(exc, McpError):
683708
logger.error(
684-
"Failed to load tools from %s (HTTP %d): %s",
709+
"Failed to load tools from %s — JSON-RPC %d: %s",
685710
ord_id,
686-
exc.response.status_code,
687-
exc.response.text[:500],
711+
exc.error.code,
712+
exc.error.message,
688713
)
689714
else:
690-
logger.exception(
691-
"Failed to load tools from %s — skipping", ord_id, exc_info=exc
715+
logger.error(
716+
"Failed to load tools from %s — %s: %s",
717+
ord_id,
718+
type(exc).__name__,
719+
exc,
720+
exc_info=exc,
692721
)
693722

694723

@@ -796,13 +825,20 @@ async def call_mcp_tool_customer(
796825
result = await session.call_tool(tool.name, kwargs)
797826

798827
if not result.content:
799-
logger.warning("Tool '%s' returned empty content", tool.name)
828+
logger.warning(
829+
"Tool '%s' on '%s' returned empty content", tool.name, tool.url
830+
)
800831
return ""
801832

802833
first = result.content[0]
803834
text = str(getattr(first, "text", ""))
804835

805836
if result.isError:
806-
logger.error("Tool '%s' returned an error: %s", tool.name, text)
837+
logger.error(
838+
"Tool '%s' on '%s' returned an error: %s",
839+
tool.name,
840+
tool.url,
841+
text,
842+
)
807843

808844
return text

‎src/sap_cloud_sdk/agentgateway/_lob.py‎

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import httpx
1414
from mcp import ClientSession
1515
from mcp.client.streamable_http import streamable_http_client
16+
from mcp.shared.exceptions import McpError
1617
from sap_cloud_sdk.destination import (
1718
create_client as create_destination_client,
1819
ConsumptionLevel,
@@ -29,6 +30,7 @@
2930
list_a2a_fragments,
3031
)
3132
from sap_cloud_sdk.agentgateway._models import (
33+
JsonRpcError,
3234
Agent,
3335
AgentCard,
3436
AgentCardFilter,
@@ -307,16 +309,41 @@ def _log_mcp_server_error(fragment_name: str, exc: BaseException) -> None:
307309
_log_mcp_server_error(fragment_name, inner)
308310
return
309311
if isinstance(exc, httpx.HTTPStatusError):
312+
try:
313+
body = exc.response.text
314+
except httpx.ResponseNotRead:
315+
body = None
316+
rpc_error = JsonRpcError.parse(body) if body else None
317+
if rpc_error:
318+
logger.error(
319+
"Failed to load tools from fragment '%s' — %s returned HTTP %d [JSON-RPC %d]: %s",
320+
fragment_name,
321+
exc.request.url,
322+
exc.response.status_code,
323+
rpc_error.code,
324+
rpc_error.message,
325+
)
326+
else:
327+
logger.error(
328+
"Failed to load tools from fragment '%s' — %s returned HTTP %d: %s",
329+
fragment_name,
330+
exc.request.url,
331+
exc.response.status_code,
332+
body[:500] if body else "(response body not available)",
333+
)
334+
elif isinstance(exc, McpError):
310335
logger.error(
311-
"Failed to load tools from fragment '%s' (HTTP %d): %s",
336+
"Failed to load tools from fragment '%s' — JSON-RPC %d: %s",
312337
fragment_name,
313-
exc.response.status_code,
314-
exc.response.text[:500],
338+
exc.error.code,
339+
exc.error.message,
315340
)
316341
else:
317-
logger.exception(
318-
"Failed to load tools from fragment '%s' — skipping",
342+
logger.error(
343+
"Failed to load tools from fragment '%s' — %s: %s",
319344
fragment_name,
345+
type(exc).__name__,
346+
exc,
320347
exc_info=exc,
321348
)
322349

@@ -482,13 +509,20 @@ async def call_mcp_tool_lob(
482509
await session.initialize()
483510
result = await session.call_tool(tool.name, kwargs)
484511
if not result.content:
485-
logger.warning("Tool '%s' returned empty content", tool.name)
512+
logger.warning(
513+
"Tool '%s' on '%s' returned empty content", tool.name, tool.url
514+
)
486515
return ""
487516
first = result.content[0]
488517
text = str(getattr(first, "text", ""))
489518

490519
if result.isError:
491-
logger.error("Tool '%s' returned an error: %s", tool.name, text)
520+
logger.error(
521+
"Tool '%s' on '%s' returned an error: %s",
522+
tool.name,
523+
tool.url,
524+
text,
525+
)
492526

493527
return text
494528

‎src/sap_cloud_sdk/agentgateway/_models.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Data models for Agent Gateway MCP tools."""
22

3+
import json
34
from dataclasses import dataclass, field
45
from typing import Any
56

@@ -96,6 +97,33 @@ class CustomerCredentials:
9697
private_key: str | None = None
9798

9899

100+
@dataclass
101+
class JsonRpcError:
102+
"""Parsed JSON-RPC error from an Agent Gateway response.
103+
104+
AGW returns HTTP 200 with a JSON-RPC error body when the request is
105+
structurally valid but the server encountered an error.
106+
107+
Example: {"jsonrpc": "2.0", "error": {"code": -32603, "message": "Internal Server Error"}}
108+
109+
Attributes:
110+
code: JSON-RPC error code.
111+
message: Human-readable error message from AGW.
112+
"""
113+
114+
code: int
115+
message: str
116+
117+
@classmethod
118+
def parse(cls, text: str) -> "JsonRpcError | None":
119+
try:
120+
data = json.loads(text)
121+
error = data.get("error", {})
122+
return cls(code=error["code"], message=error["message"])
123+
except Exception:
124+
return None
125+
126+
99127
@dataclass
100128
class AgentCard:
101129
"""Agent Card as returned by the A2A well-known endpoint.

‎src/sap_cloud_sdk/agentgateway/agw_client.py‎

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -284,10 +284,14 @@ async def get_user_auth(
284284
self._config.timeout,
285285
self._token_cache,
286286
)
287-
return AuthResult(
287+
result = AuthResult(
288288
access_token=token,
289289
gateway_url=credentials.gateway_url,
290290
)
291+
logger.info(
292+
"User auth token obtained — gateway: '%s'", result.gateway_url
293+
)
294+
return result
291295

292296
# Check for transparent mode
293297
if detect_transparent_credentials():
@@ -302,10 +306,14 @@ async def get_user_auth(
302306
self._config.timeout,
303307
self._token_cache,
304308
)
305-
return AuthResult(
309+
result = AuthResult(
306310
access_token=token,
307311
gateway_url=credentials.gateway_url,
308312
)
313+
logger.info(
314+
"User auth token obtained — gateway: '%s'", result.gateway_url
315+
)
316+
return result
309317

310318
tenant = self._resolve_tenant_subdomain()
311319
token, gateway_url = await fetch_user_auth(
@@ -314,7 +322,9 @@ async def get_user_auth(
314322
token_cache=self._token_cache,
315323
gateway_url_cache=self._gateway_url_cache,
316324
)
317-
return AuthResult(access_token=token, gateway_url=gateway_url)
325+
result = AuthResult(access_token=token, gateway_url=gateway_url)
326+
logger.info("User auth token obtained — gateway: '%s'", result.gateway_url)
327+
return result
318328

319329
except AgentGatewaySDKError:
320330
raise
@@ -444,10 +454,6 @@ async def list_mcp_tools(
444454

445455
# LoB flow - requires tenant_subdomain
446456
tenant = self._resolve_tenant_subdomain()
447-
if user_token:
448-
auth = await self.get_user_auth(user_token)
449-
else:
450-
auth = await self.get_system_auth()
451457
return await get_mcp_tools_lob(
452458
tenant,
453459
auth.access_token,
@@ -597,7 +603,10 @@ async def call_mcp_tool(
597603
tool, auth.access_token, self._config.timeout, **kwargs
598604
)
599605

600-
auth = await self.get_user_auth(user_token)
606+
if not user_token:
607+
raise AgentGatewaySDKError(
608+
"user_token is required for LoB tool invocation."
609+
)
601610
return await call_mcp_tool_lob(
602611
tool, auth.access_token, self._config.timeout, **kwargs
603612
)

‎src/sap_cloud_sdk/core/runtime_context/__init__.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@
2121
from sap_cloud_sdk.core.runtime_context._context import (
2222
RuntimeContext,
2323
get_context,
24+
is_feature_enabled,
2425
)
2526
from sap_cloud_sdk.core.runtime_context._envelope import RequestEnvelope
2627
from sap_cloud_sdk.core.runtime_context._keys import (
2728
ContextKey,
2829
DWC_SUBDOMAIN,
2930
DWC_TENANT,
31+
FEATURE_TOGGLES,
3032
TRIGGER_TYPE,
3133
)
3234
from sap_cloud_sdk.core.runtime_context._protocol import ContextProvider
@@ -50,6 +52,7 @@
5052
"DWC_SUBDOMAIN",
5153
"DWC_TENANT",
5254
"DWCContextProvider",
55+
"FEATURE_TOGGLES",
5356
"FrameworkAdapter",
5457
"GLOBAL_TENANT_ID",
5558
"IASContextProvider",
@@ -59,5 +62,6 @@
5962
"TRIGGER_TYPE",
6063
"USER_ID",
6164
"get_context",
65+
"is_feature_enabled",
6266
"register",
6367
]

‎src/sap_cloud_sdk/core/runtime_context/_context.py‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from contextvars import ContextVar
55
from typing import Any, AsyncGenerator, Dict, Generator, Optional, TypeVar
66

7-
from sap_cloud_sdk.core.runtime_context._keys import ContextKey
7+
from sap_cloud_sdk.core.runtime_context._keys import ContextKey, FEATURE_TOGGLES
88

99
T = TypeVar("T")
1010

@@ -64,6 +64,17 @@ def get_context() -> RuntimeContext:
6464
return _context_var.get()
6565

6666

67+
def is_feature_enabled(name: str) -> bool:
68+
"""Return ``True`` if *name* is in the active feature toggles for the current request.
69+
70+
Feature toggles are populated from the ``dwc-stage-configuration`` DWC request
71+
header by :class:`~sap_cloud_sdk.core.runtime_context.DWCContextProvider`.
72+
Returns ``False`` when no toggles header was present or the toggle is absent.
73+
"""
74+
toggles = get_context().get(FEATURE_TOGGLES)
75+
return name in toggles if toggles is not None else False
76+
77+
6778
@contextmanager
6879
def sdk_context(ctx: RuntimeContext) -> Generator[RuntimeContext, None, None]:
6980
"""Sync context manager that sets *ctx* for the duration of the block."""

‎src/sap_cloud_sdk/core/runtime_context/_keys.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Typed context key for RuntimeContext."""
22

3-
from typing import Generic, TypeVar
3+
from typing import Generic, List, TypeVar
44

55
T = TypeVar("T")
66

@@ -37,3 +37,4 @@ def __repr__(self) -> str:
3737
TRIGGER_TYPE = ContextKey[str]("trigger_type")
3838
DWC_SUBDOMAIN = ContextKey[str]("dwc_subdomain")
3939
DWC_TENANT = ContextKey[str]("dwc_tenant")
40+
FEATURE_TOGGLES = ContextKey[List[str]]("dwc_feature_toggles")

0 commit comments

Comments
 (0)