Skip to content

Commit 5cb795f

Browse files
committed
merge
2 parents bbfc73b + 101492b commit 5cb795f

12 files changed

Lines changed: 415 additions & 279 deletions

File tree

.env_integration_tests.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ CLOUD_SDK_CFG_ADMS_DEFAULT_RESOURCE=urn:sap:identity:application:provider:name:y
1111
CLOUD_SDK_CFG_AGW_DEFAULT_LANDSCAPE=your-landscape-here
1212
CLOUD_SDK_CFG_AGW_DEFAULT_TENANT_SUBDOMAIN=your-tenant-subdomain-here
1313
CLOUD_SDK_CFG_AGW_DEFAULT_USER_TOKEN=your-user-jwt-here
14+
CLOUD_SDK_CFG_AGW_DEFAULT_SAMPLE_MCP_TOOL=your-sample-mcp-tool-name-here
1415

1516
# AGENT MEMORY
1617
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_APPLICATION_URL=https://your-agent-memory-api-url-here

docs/DEVELOPMENT.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Tip:
3434
## Type Check
3535

3636
```bash
37-
uv run ty check .
37+
uvx ty check .
3838
```
3939

4040
## Code Quality Checks
@@ -49,7 +49,7 @@ uv run ruff check .
4949
uv run ruff format --check .
5050

5151
# Type check
52-
uv run ty check .
52+
uvx ty check .
5353
```
5454

5555
## Pre-commit Hooks (Recommended)

docs/INTEGRATION_TESTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ CLOUD_SDK_CFG_AGW_DEFAULT_TENANT_SUBDOMAIN=your-tenant-subdomain-here
6262
# User JWT for token exchange scenarios (get_user_auth)
6363
# If not set, user auth scenarios are automatically skipped
6464
CLOUD_SDK_CFG_AGW_DEFAULT_USER_TOKEN=your-user-jwt-here
65+
66+
# Name of a MCP tool available in your environment
67+
CLOUD_SDK_CFG_AGW_DEFAULT_SAMPLE_MCP_TOOL=your-sample-mcp-tool-name-here
6568
```
6669

6770
### Agent Memory Integration Tests

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.28.0"
3+
version = "0.29.0"
44
description = "SAP Cloud SDK for Python"
55
readme = "README.md"
66
license = "Apache-2.0"

src/sap_cloud_sdk/agentgateway/converters.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from typing import TYPE_CHECKING, Any, Callable
1111

12-
from pydantic import create_model
12+
from pydantic import Field, create_model
1313

1414
from sap_cloud_sdk.agentgateway._models import MCPTool
1515

@@ -21,6 +21,8 @@ def mcp_tool_to_langchain(
2121
mcp_tool: MCPTool,
2222
call_tool: Callable,
2323
get_user_token: Callable[[], str],
24+
*,
25+
omit_none: bool = True,
2426
) -> StructuredTool:
2527
"""Convert MCPTool to LangChain StructuredTool.
2628
@@ -31,6 +33,8 @@ def mcp_tool_to_langchain(
3133
mcp_tool: MCPTool object from list_mcp_tools().
3234
call_tool: Callable to invoke the MCP tool (e.g., agw_client.call_mcp_tool).
3335
get_user_token: Callable that returns the user's JWT token.
36+
omit_none: If True (default), optional parameters with a None value are not
37+
forwarded to call_tool. Set to False to forward None values explicitly.
3438
3539
Returns:
3640
LangChain StructuredTool that invokes the MCP tool.
@@ -66,17 +70,21 @@ def mcp_tool_to_langchain(
6670
) from None
6771

6872
async def run(**kwargs) -> str:
73+
resolved = (
74+
{k: v for k, v in kwargs.items() if v is not None} if omit_none else kwargs
75+
)
6976
return await call_tool(
7077
mcp_tool,
7178
user_token=get_user_token,
72-
**kwargs,
79+
**resolved,
7380
)
7481

7582
# Build args schema from input_schema
7683
properties = mcp_tool.input_schema.get("properties", {})
7784
required = set(mcp_tool.input_schema.get("required", []))
7885
fields: dict[str, Any] = {
79-
k: (str, ...) if k in required else (str | None, None) for k in properties
86+
k: (str, ...) if k in required else (str | None, Field(default=None))
87+
for k in properties
8088
}
8189
args_schema = create_model(f"{mcp_tool.name}_args", **fields) if fields else None
8290

src/sap_cloud_sdk/agentgateway/user-guide.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,7 @@ config = ClientConfig(timeout=30.0)
5252
agw_client = create_client(tenant_subdomain="my-tenant", config=config)
5353

5454
# Discover tools (auto-discovered from destination fragments)
55-
tools = await agw_client.list_mcp_tools()
56-
57-
# Discover tools with user principal propagation
55+
# Pass user_token to use principal propagation when listing tools
5856
tools = await agw_client.list_mcp_tools(user_token="user-jwt")
5957

6058
# Invoke a tool (user_token required for principal propagation)
@@ -74,7 +72,7 @@ from sap_cloud_sdk.agentgateway import create_client
7472
from sap_cloud_sdk.agentgateway.converters import mcp_tool_to_langchain
7573

7674
agw_client = create_client(tenant_subdomain="my-tenant")
77-
tools = await agw_client.list_mcp_tools()
75+
tools = await agw_client.list_mcp_tools(user_token="user-jwt")
7876

7977
langchain_tools = [
8078
mcp_tool_to_langchain(
@@ -89,6 +87,17 @@ langchain_tools = [
8987
llm_with_tools = llm.bind_tools(langchain_tools)
9088
```
9189

90+
By default, optional tool parameters that resolve to `None` are not forwarded to `call_mcp_tool`. Set `omit_none=False` to forward them explicitly:
91+
92+
```python
93+
mcp_tool_to_langchain(
94+
t,
95+
agw_client.call_mcp_tool,
96+
get_user_token=lambda: request.headers["Authorization"],
97+
omit_none=False,
98+
)
99+
```
100+
92101
## Concepts
93102

94103
### Agent Types

tests/adms/unit/test_client.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ def _make_httpx_response(
8888

8989
def _make_token_fetcher(config: AdmsConfig) -> IasTokenFetcher:
9090
fetcher = IasTokenFetcher(config=config)
91-
fetcher.get_token = MagicMock(return_value="test-bearer-token") # type: ignore[method-assign] # ty: ignore[invalid-assignment]
92-
fetcher.exchange_token = MagicMock(return_value="user-bearer-token") # type: ignore[method-assign] # ty: ignore[invalid-assignment]
91+
fetcher.get_token = MagicMock(return_value="test-bearer-token") # type: ignore[method-assign]
92+
fetcher.exchange_token = MagicMock(return_value="user-bearer-token") # type: ignore[method-assign]
9393
return fetcher
9494

9595

@@ -423,13 +423,13 @@ def test_with_user_jwt_returns_new_instance(self, config):
423423
http = _make_async_http(config, fetcher)
424424
mock_user_http = MagicMock(spec=AsyncAdmsHttp)
425425
mock_user_http._client = AsyncMock(spec=httpx.AsyncClient)
426-
http.with_user_jwt = MagicMock(return_value=mock_user_http) # type: ignore[method-assign] # ty: ignore[invalid-assignment]
426+
http.with_user_jwt = MagicMock(return_value=mock_user_http) # type: ignore[method-assign]
427427

428428
client = AsyncAdmsClient(http)
429429
new_client = client.with_user_jwt("my-jwt")
430430

431431
assert new_client is not client
432-
http.with_user_jwt.assert_called_once_with("my-jwt") # type: ignore[union-attr] # ty: ignore[unresolved-attribute]
432+
http.with_user_jwt.assert_called_once_with("my-jwt") # type: ignore[union-attr]
433433
assert new_client._http is mock_user_http
434434

435435
@pytest.mark.asyncio

tests/agentgateway/integration/agw_auth.feature

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,17 @@ Feature: Agent Gateway Auth Integration
4141
And the error message should mention "user_token is required"
4242

4343
Scenario: List MCP tools returns a non-empty list of tools
44+
Given I have a valid user token
4445
When I call list_mcp_tools
4546
Then the result should be a list of MCPTool
4647
And the list should be non-empty
4748
And each tool should have a non-empty name
4849
And each tool should have a non-empty url
50+
And each tool should have a valid input_schema
4951

50-
Scenario: Call search_workflows tool returns a non-empty result
52+
Scenario: Call sample MCP tool returns a non-empty result
5153
Given I have a valid user token
54+
And I have a sample MCP tool name
5255
When I call list_mcp_tools
53-
And I call call_mcp_tool with "search_workflows" and the user token
56+
And I call call_mcp_tool with the sample MCP tool and the user token
5457
Then the tool result should be a non-empty string

tests/agentgateway/integration/test_agw_bdd.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
CLOUD_SDK_CFG_AGW_DEFAULT_TENANT_SUBDOMAIN=<tenant-subdomain> \\
66
CLOUD_SDK_CFG_AGW_DEFAULT_LANDSCAPE=<landscape> \\
77
CLOUD_SDK_CFG_AGW_DEFAULT_USER_TOKEN=<user-jwt> \\
8+
CLOUD_SDK_CFG_AGW_DEFAULT_SAMPLE_MCP_TOOL=<tool-name> \\
89
CLOUD_SDK_CFG_DESTINATION_DEFAULT_CLIENTID=... \\
910
CLOUD_SDK_CFG_DESTINATION_DEFAULT_CLIENTSECRET=... \\
1011
CLOUD_SDK_CFG_DESTINATION_DEFAULT_URL=... \\
@@ -52,6 +53,7 @@ def __init__(self):
5253
self.user_token: Optional[str] = None
5354
self.tools: Optional[list[MCPTool]] = None
5455
self.tool_result: Optional[str] = None
56+
self.sample_mcp_tool_name: Optional[str] = None
5557

5658

5759
@pytest.fixture
@@ -78,6 +80,15 @@ def have_valid_user_token(context: ScenarioContext):
7880
context.user_token = token
7981

8082

83+
@given("I have a sample MCP tool name")
84+
def have_sample_mcp_tool_name(context: ScenarioContext):
85+
"""Load sample MCP tool name from environment variable."""
86+
tool_name = os.environ.get("CLOUD_SDK_CFG_AGW_DEFAULT_SAMPLE_MCP_TOOL", "")
87+
if not tool_name:
88+
pytest.skip("CLOUD_SDK_CFG_AGW_DEFAULT_SAMPLE_MCP_TOOL is not set — skipping tool scenario")
89+
context.sample_mcp_tool_name = tool_name
90+
91+
8192
# ==================== WHEN ====================
8293

8394

@@ -119,18 +130,17 @@ def call_get_user_auth_empty_token(context: ScenarioContext, agw_client: AgentGa
119130
@when("I call list_mcp_tools")
120131
def call_list_mcp_tools(context: ScenarioContext, agw_client: AgentGatewayClient):
121132
"""Call list_mcp_tools and store the result."""
122-
context.tools = run(agw_client.list_mcp_tools())
133+
context.tools = run(agw_client.list_mcp_tools(user_token=context.user_token))
123134

124135

125-
@when(parsers.parse('I call call_mcp_tool with "{tool_name}" and the user token'))
126-
def call_call_mcp_tool(
127-
context: ScenarioContext, agw_client: AgentGatewayClient, tool_name: str
128-
):
129-
"""Find tool by name from list_mcp_tools result and call it."""
136+
@when("I call call_mcp_tool with the sample MCP tool and the user token")
137+
def call_call_mcp_tool_sample(context: ScenarioContext, agw_client: AgentGatewayClient):
138+
"""Find the sample MCP tool and call it."""
130139
assert context.tools is not None, "call list_mcp_tools before calling a tool"
131-
tool = next((t for t in context.tools if t.name == tool_name), None)
140+
assert context.sample_mcp_tool_name is not None
141+
tool = next((t for t in context.tools if t.name == context.sample_mcp_tool_name), None)
132142
if tool is None:
133-
pytest.fail(f"Tool '{tool_name}' not found in list_mcp_tools result")
143+
pytest.fail(f"Tool '{context.sample_mcp_tool_name}' not found in list_mcp_tools result")
134144
context.tool_result = run(
135145
agw_client.call_mcp_tool(tool, user_token=context.user_token)
136146
)
@@ -237,6 +247,19 @@ def each_tool_has_non_empty_url(context: ScenarioContext):
237247
)
238248

239249

250+
@then("each tool should have a valid input_schema")
251+
def each_tool_has_valid_input_schema(context: ScenarioContext):
252+
"""Verify every tool has an input_schema dict with type=object."""
253+
assert context.tools is not None
254+
for tool in context.tools:
255+
assert isinstance(tool.input_schema, dict), (
256+
f"Tool '{tool.name}' input_schema is not a dict: {tool.input_schema!r}"
257+
)
258+
assert tool.input_schema.get("type") == "object", (
259+
f"Tool '{tool.name}' input_schema missing type=object: {tool.input_schema}"
260+
)
261+
262+
240263
@then("the tool result should be a non-empty string")
241264
def tool_result_is_non_empty_string(context: ScenarioContext):
242265
"""Verify the tool invocation returned a non-empty string."""

0 commit comments

Comments
 (0)