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=... \\
1516
1617import asyncio
1718import os
18- from typing import Optional
19+ from typing import Optional , Any
1920
2021import pytest
2122from pytest_bdd import scenarios , given , when , then , parsers
2223
2324from sap_cloud_sdk .agentgateway import AgentGatewayClient , AuthResult , AgentGatewaySDKError
2425from sap_cloud_sdk .agentgateway ._models import MCPTool
26+ from sap_cloud_sdk .agentgateway .converters import mcp_tool_to_langchain
2527
2628scenarios ("agw_auth.feature" )
2729
@@ -52,6 +54,8 @@ def __init__(self):
5254 self .user_token : Optional [str ] = None
5355 self .tools : Optional [list [MCPTool ]] = None
5456 self .tool_result : Optional [str ] = None
57+ self .langchain_tool : Optional [Any ] = None
58+ self .sample_mcp_tool_name : Optional [str ] = None
5559
5660
5761@pytest .fixture
@@ -78,6 +82,15 @@ def have_valid_user_token(context: ScenarioContext):
7882 context .user_token = token
7983
8084
85+ @given ("I have a sample MCP tool name" )
86+ def have_sample_mcp_tool_name (context : ScenarioContext ):
87+ """Load sample MCP tool name from environment variable."""
88+ tool_name = os .environ .get ("CLOUD_SDK_CFG_AGW_DEFAULT_SAMPLE_MCP_TOOL" , "" )
89+ if not tool_name :
90+ pytest .skip ("CLOUD_SDK_CFG_AGW_DEFAULT_SAMPLE_MCP_TOOL is not set — skipping tool scenario" )
91+ context .sample_mcp_tool_name = tool_name
92+
93+
8194# ==================== WHEN ====================
8295
8396
@@ -119,7 +132,7 @@ def call_get_user_auth_empty_token(context: ScenarioContext, agw_client: AgentGa
119132@when ("I call list_mcp_tools" )
120133def call_list_mcp_tools (context : ScenarioContext , agw_client : AgentGatewayClient ):
121134 """Call list_mcp_tools and store the result."""
122- context .tools = run (agw_client .list_mcp_tools ())
135+ context .tools = run (agw_client .list_mcp_tools (user_token = context . user_token ))
123136
124137
125138@when (parsers .parse ('I call call_mcp_tool with "{tool_name}" and the user token' ))
@@ -136,6 +149,34 @@ def call_call_mcp_tool(
136149 )
137150
138151
152+ @when ("I call call_mcp_tool with the sample MCP tool and the user token" )
153+ def call_call_mcp_tool_sample (context : ScenarioContext , agw_client : AgentGatewayClient ):
154+ """Find the sample MCP tool and call it."""
155+ assert context .tools is not None , "call list_mcp_tools before calling a tool"
156+ assert context .sample_mcp_tool_name is not None
157+ tool = next ((t for t in context .tools if t .name == context .sample_mcp_tool_name ), None )
158+ if tool is None :
159+ pytest .fail (f"Tool '{ context .sample_mcp_tool_name } ' not found in list_mcp_tools result" )
160+ context .tool_result = run (
161+ agw_client .call_mcp_tool (tool , user_token = context .user_token )
162+ )
163+
164+
165+ @when ("I convert the sample MCP tool to a LangChain StructuredTool" )
166+ def convert_sample_tool_to_langchain (context : ScenarioContext , agw_client : AgentGatewayClient ):
167+ """Convert the sample MCP tool to a LangChain StructuredTool."""
168+ assert context .tools is not None , "call list_mcp_tools before converting a tool"
169+ assert context .sample_mcp_tool_name is not None
170+ tool = next ((t for t in context .tools if t .name == context .sample_mcp_tool_name ), None )
171+ if tool is None :
172+ pytest .fail (f"Tool '{ context .sample_mcp_tool_name } ' not found in list_mcp_tools result" )
173+ context .langchain_tool = mcp_tool_to_langchain (
174+ tool ,
175+ agw_client .call_mcp_tool ,
176+ get_user_token = lambda : context .user_token ,
177+ )
178+
179+
139180# ==================== THEN ====================
140181
141182
@@ -237,9 +278,71 @@ def each_tool_has_non_empty_url(context: ScenarioContext):
237278 )
238279
239280
281+ @then ("each tool should have a valid input_schema" )
282+ def each_tool_has_valid_input_schema (context : ScenarioContext ):
283+ """Verify every tool has an input_schema dict with type=object."""
284+ assert context .tools is not None
285+ for tool in context .tools :
286+ assert isinstance (tool .input_schema , dict ), (
287+ f"Tool '{ tool .name } ' input_schema is not a dict: { tool .input_schema !r} "
288+ )
289+ assert tool .input_schema .get ("type" ) == "object" , (
290+ f"Tool '{ tool .name } ' input_schema missing type=object: { tool .input_schema } "
291+ )
292+
293+
240294@then ("the tool result should be a non-empty string" )
241295def tool_result_is_non_empty_string (context : ScenarioContext ):
242296 """Verify the tool invocation returned a non-empty string."""
243297 assert context .tool_result is not None
244298 assert isinstance (context .tool_result , str )
245299 assert context .tool_result .strip (), "Expected a non-empty tool result"
300+
301+
302+ @when (parsers .parse ('I convert the "{tool_name}" tool to a LangChain StructuredTool' ))
303+ def convert_tool_to_langchain (
304+ context : ScenarioContext , agw_client : AgentGatewayClient , tool_name : str
305+ ):
306+ """Convert the named MCPTool to a LangChain StructuredTool."""
307+ assert context .tools is not None , "call list_mcp_tools before converting a tool"
308+ tool = next ((t for t in context .tools if t .name == tool_name ), None )
309+ if tool is None :
310+ pytest .fail (f"Tool '{ tool_name } ' not found in list_mcp_tools result" )
311+ context .langchain_tool = mcp_tool_to_langchain (
312+ tool ,
313+ agw_client .call_mcp_tool ,
314+ get_user_token = lambda : context .user_token ,
315+ )
316+
317+
318+ @then (parsers .parse ('the LangChain tool should have name "{expected_name}"' ))
319+ def langchain_tool_has_name (context : ScenarioContext , expected_name : str ):
320+ """Verify the converted LangChain tool has the expected name."""
321+ assert context .langchain_tool is not None
322+ assert context .langchain_tool .name == expected_name
323+
324+
325+ @then ("the LangChain tool should have the sample MCP tool name" )
326+ def langchain_tool_has_sample_name (context : ScenarioContext ):
327+ """Verify the converted LangChain tool name matches the sample MCP tool name."""
328+ assert context .langchain_tool is not None
329+ assert context .langchain_tool .name == context .sample_mcp_tool_name
330+
331+
332+ @then ("the LangChain tool should have a non-empty description" )
333+ def langchain_tool_has_description (context : ScenarioContext ):
334+ """Verify the converted LangChain tool has a non-empty description."""
335+ assert context .langchain_tool is not None
336+ assert isinstance (context .langchain_tool .description , str )
337+ assert context .langchain_tool .description .strip ()
338+
339+
340+ @then ("the LangChain tool should require no arguments" )
341+ def langchain_tool_requires_no_arguments (context : ScenarioContext ):
342+ """Verify the converted LangChain tool has no required fields in its args schema."""
343+ assert context .langchain_tool is not None
344+ schema = context .langchain_tool .args_schema
345+ if schema is not None :
346+ fields = schema .model_fields
347+ required = [name for name , f in fields .items () if f .is_required ()]
348+ assert required == [], f"Expected no required args, got: { required } "
0 commit comments