Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "sap-cloud-sdk"
version = "0.32.1"
version = "0.33.0"
description = "SAP Cloud SDK for Python"
readme = "README.md"
license = "Apache-2.0"
Expand Down
2 changes: 2 additions & 0 deletions src/sap_cloud_sdk/agentgateway/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
from sap_cloud_sdk.agentgateway._models import (
AuthResult,
MCPTool,
MCPToolFilter,
Agent,
AgentCard,
AgentCardFilter,
Expand All @@ -77,6 +78,7 @@
# Data models
"AuthResult",
"MCPTool",
"MCPToolFilter",
"Agent",
"AgentCard",
"AgentCardFilter",
Expand Down
15 changes: 15 additions & 0 deletions src/sap_cloud_sdk/agentgateway/_customer.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
CustomerCredentials,
IntegrationDependency,
MCPTool,
MCPToolFilter,
)
from sap_cloud_sdk.agentgateway._token_cache import _TokenCache
from sap_cloud_sdk.agentgateway.exceptions import AgentGatewaySDKError
Expand Down Expand Up @@ -480,6 +481,7 @@ async def get_mcp_tools_customer(
credentials: CustomerCredentials,
system_token: str,
timeout: float,
filter: MCPToolFilter | None = None,
) -> list[MCPTool]:
"""List all MCP tools from servers defined in credentials.

Expand All @@ -490,20 +492,28 @@ async def get_mcp_tools_customer(
credentials: Customer credentials with integrationDependencies.
system_token: Pre-fetched raw system token for authentication.
timeout: HTTP timeout in seconds for MCP server calls.
filter: Optional MCPToolFilter narrowing results by tool name or ORD ID.
If None or empty, all tools are included.

Returns:
List of MCPTool objects from all servers.

Raises:
AgentGatewaySDKError: If integrationDependencies is empty.
"""
f = filter or MCPToolFilter()
dependencies = credentials.integration_dependencies

if not dependencies:
raise AgentGatewaySDKError(
"integrationDependencies is empty in credentials — no MCP servers configured."
)

# Pre-fetch filter: ORD ID is extractable from the URL without fetching tools
if f.ord_ids:
ord_ids_set = set(f.ord_ids)
dependencies = [d for d in dependencies if d.ord_id in ord_ids_set]

logger.info("Discovering tools from %d MCP server(s)", len(dependencies))

tools: list[MCPTool] = []
Expand All @@ -524,6 +534,11 @@ async def get_mcp_tools_customer(
except Exception:
logger.exception("Failed to load tools from %s — skipping", dep.ord_id)

# Post-fetch filter: tool names are only known after fetching
if f.names:
names_set = set(f.names)
tools = [t for t in tools if t.name in names_set]

logger.info(
"Loaded %d MCP tool(s) from %d server(s)", len(tools), len(dependencies)
)
Expand Down
54 changes: 39 additions & 15 deletions src/sap_cloud_sdk/agentgateway/_lob.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@
list_mcp_fragments,
list_a2a_fragments,
)
from sap_cloud_sdk.agentgateway._models import Agent, AgentCard, MCPTool
from sap_cloud_sdk.agentgateway._models import (
Agent,
AgentCard,
AgentCardFilter,
MCPTool,
MCPToolFilter,
)
from sap_cloud_sdk.agentgateway._token_cache import _GatewayUrlCache, _TokenCache
from sap_cloud_sdk.agentgateway.exceptions import (
AgentGatewaySDKError,
Expand Down Expand Up @@ -308,6 +314,7 @@ async def get_mcp_tools_lob(
tenant_subdomain: str,
system_token: str,
timeout: float,
filter: MCPToolFilter | None = None,
) -> list[MCPTool]:
"""List all MCP tools using LoB flow (destination-based).

Expand All @@ -317,10 +324,13 @@ async def get_mcp_tools_lob(
tenant_subdomain: Tenant subdomain for multi-tenant lookup.
system_token: Pre-fetched raw system token (from get_system_auth).
timeout: HTTP timeout in seconds for MCP server calls.
filter: Optional MCPToolFilter narrowing results by tool name or ORD ID.
If None or empty, all tools are included.

Returns:
List of MCPTool objects from all MCP servers.
"""
f = filter or MCPToolFilter()
tools: list[MCPTool] = []
loop = asyncio.get_running_loop()

Expand All @@ -334,6 +344,18 @@ async def get_mcp_tools_lob(
)
return tools

# Pre-fetch filter: ORD ID is extractable from the URL without fetching tools
Comment thread
jplbrun marked this conversation as resolved.
if f.ord_ids:
ord_ids_set = set(f.ord_ids)
fragments = [
fr
for fr in fragments
if _ord_id_from_url(
fr.properties.get("URL") or fr.properties.get("url") or ""
)
in ord_ids_set
]

for fragment in fragments:
fragment_name = fragment.name
mcp_url = fragment.properties.get("URL") or fragment.properties.get("url")
Expand All @@ -360,6 +382,11 @@ async def get_mcp_tools_lob(
fragment_name,
)

# Post-fetch filter: tool names are only known after fetching
if f.names:
names_set = set(f.names)
tools = [t for t in tools if t.name in names_set]

logger.info("Loaded %d MCP tool(s) from %d fragment(s)", len(tools), len(fragments))
return tools

Expand Down Expand Up @@ -478,8 +505,7 @@ async def get_agent_cards_lob(
tenant_subdomain: str,
system_token: str,
timeout: float,
agent_names: list[str] | None = None,
ord_ids: list[str] | None = None,
filter: AgentCardFilter | None = None,
) -> list[Agent]:
"""List A2A agents and their agent cards using LoB flow.

Expand All @@ -496,15 +522,13 @@ async def get_agent_cards_lob(
tenant_subdomain: Tenant subdomain for multi-tenant lookup.
system_token: Pre-fetched raw system token for authentication.
timeout: HTTP timeout in seconds.
agent_names: Optional list of agent card names to include (matched
against the `name` field in the fetched agent card JSON).
Applied after fetching. If empty or None, all are included.
ord_ids: Optional list of ORD IDs to include (extracted from URL).
Applied before fetching. If empty or None, all are included.
filter: Optional AgentCardFilter narrowing results by agent card name
or ORD ID. If None or empty, all A2A fragments are included.

Returns:
List of Agent objects, each containing ORD ID and fetched AgentCard.
"""
f = filter or AgentCardFilter()
loop = asyncio.get_running_loop()

logger.info("Listing A2A fragments for tenant '%s'", tenant_subdomain)
Expand All @@ -517,13 +541,13 @@ async def get_agent_cards_lob(
return []

# Pre-fetch filter: ORD ID is extractable from the URL without fetching the card
if ord_ids:
ord_ids_set = set(ord_ids)
if f.ord_ids:
ord_ids_set = set(f.ord_ids)
fragments = [
f
for f in fragments
fr
for fr in fragments
if _ord_id_from_url(
{k.lower(): v for k, v in f.properties.items()}.get("url", "")
{k.lower(): v for k, v in fr.properties.items()}.get("url", "")
)
in ord_ids_set
]
Expand Down Expand Up @@ -562,8 +586,8 @@ async def get_agent_cards_lob(
)

# Post-fetch filter: agent card name is only known after fetching
if agent_names:
agent_names_set = set(agent_names)
if f.agent_names:
agent_names_set = set(f.agent_names)
agents = [a for a in agents if a.agent_card.raw.get("name") in agent_names_set]

logger.info(
Expand Down
33 changes: 33 additions & 0 deletions src/sap_cloud_sdk/agentgateway/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,36 @@ class AgentCardFilter:

agent_names: list[str] = field(default_factory=list)
ord_ids: list[str] = field(default_factory=list)


@dataclass
class MCPToolFilter:
"""Filter options for list_mcp_tools.

All fields are optional. When multiple fields are set they are applied
together (AND semantics). Empty lists are treated the same as None (no
filtering on that field).

Attributes:
names: Tool names to include (matched against MCPTool.name).
Applied after fetching.
ord_ids: ORD IDs to include (extracted from the fragment URL for LoB
agents, or matched against IntegrationDependency.ord_id for
customer agents). Applied before fetching, skipping non-matching
fragments.

Example:
```python
from sap_cloud_sdk.agentgateway import MCPToolFilter

tools = await agw_client.list_mcp_tools(
filter=MCPToolFilter(
names=["get-sales-order"],
ord_ids=["sap.s4:apiAccess:salesOrder:v1"],
)
)
```
"""

names: list[str] = field(default_factory=list)
ord_ids: list[str] = field(default_factory=list)
27 changes: 22 additions & 5 deletions src/sap_cloud_sdk/agentgateway/agw_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
AgentCardFilter,
AuthResult,
MCPTool,
MCPToolFilter,
)
from sap_cloud_sdk.agentgateway._token_cache import _GatewayUrlCache, _TokenCache
from sap_cloud_sdk.agentgateway.exceptions import AgentGatewaySDKError
Expand Down Expand Up @@ -300,6 +301,7 @@ async def list_mcp_tools(
self,
user_token: str | Callable[[], str] | None = None,
app_tid: str | None = None,
filter: MCPToolFilter | None = None,
) -> list[MCPTool]:
"""List all MCP tools from MCP servers.

Expand All @@ -320,6 +322,8 @@ async def list_mcp_tools(
If provided, uses user-scoped auth instead of system auth.
app_tid: BTP Application Tenant ID of the subscriber.
Only used for customer agents.
filter: Optional filter to narrow results by tool name or ORD ID.
If None or empty, all tools are included.

Returns:
List of MCPTool objects from all MCP servers.
Expand All @@ -335,6 +339,15 @@ async def list_mcp_tools(

# With user token for principal propagation:
tools = await agw_client.list_mcp_tools(user_token="user-jwt")

# With filters
from sap_cloud_sdk.agentgateway import MCPToolFilter
tools = await agw_client.list_mcp_tools(
filter=MCPToolFilter(
names=["get-sales-order"],
ord_ids=["sap.s4:apiAccess:salesOrder:v1"],
)
)
```
"""
try:
Expand All @@ -350,7 +363,10 @@ async def list_mcp_tools(
else:
auth = await self.get_system_auth(app_tid=app_tid)
return await get_mcp_tools_customer(
credentials, auth.access_token, self._config.timeout
credentials,
auth.access_token,
self._config.timeout,
filter=filter,
)

# LoB flow - requires tenant_subdomain
Expand All @@ -363,7 +379,10 @@ async def list_mcp_tools(
else:
auth = await self.get_system_auth()
return await get_mcp_tools_lob(
tenant, auth.access_token, self._config.timeout
tenant,
auth.access_token,
self._config.timeout,
filter=filter,
)

except AgentGatewaySDKError:
Expand Down Expand Up @@ -426,13 +445,11 @@ async def list_agent_cards(

tenant = self._resolve_tenant_subdomain()
auth = await self.get_system_auth()
f = filter or AgentCardFilter()
return await get_agent_cards_lob(
tenant,
auth.access_token,
self._config.timeout,
agent_names=f.agent_names or None,
ord_ids=f.ord_ids or None,
filter=filter,
)
except AgentGatewaySDKError:
raise
Expand Down
29 changes: 27 additions & 2 deletions src/sap_cloud_sdk/agentgateway/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ result = await agw_client.call_mcp_tool(
LoB agents use BTP Destination Service for credential management. Tools and A2A agents are auto-discovered from destination fragments.

```python
from sap_cloud_sdk.agentgateway import ClientConfig, create_client
from sap_cloud_sdk.agentgateway import ClientConfig, MCPToolFilter, create_client

config = ClientConfig(timeout=30.0)
agw_client = create_client(tenant_subdomain="my-tenant", config=config)
Expand All @@ -54,6 +54,14 @@ agw_client = create_client(tenant_subdomain="my-tenant", config=config)
# Pass user_token to use principal propagation when listing tools
tools = await agw_client.list_mcp_tools(user_token="user-jwt")

# Filter by tool name (post-fetch) or ORD ID (pre-fetch)
tools = await agw_client.list_mcp_tools(
filter=MCPToolFilter(
names=["get-sales-order"],
ord_ids=["sap.s4:apiAccess:salesOrder:v1"],
)
)

# Invoke a tool (user_token required for principal propagation)
result = await agw_client.call_mcp_tool(
tool=tools[0],
Expand Down Expand Up @@ -204,6 +212,7 @@ class AgentGatewayClient:
self,
user_token: str | Callable[[], str] | None = None,
app_tid: str | None = None,
filter: MCPToolFilter | None = None,
) -> list[MCPTool]

async def call_mcp_tool(
Expand Down Expand Up @@ -231,7 +240,23 @@ AgentCardFilter(
)
```

Both fields default to empty lists. Filters are applied with AND semantics: if both are set, an agent must match both to be included. `agent_names` is applied after fetching (requires reading the card); `ord_ids` is applied before fetching (extracted from the fragment URL, no card request needed).
Both fields default to empty lists. `agent_names` is applied after fetching; `ord_ids` is applied before fetching (extracted from the fragment URL, no card request needed).

### MCPToolFilter

```python
from sap_cloud_sdk.agentgateway import MCPToolFilter

MCPToolFilter(
names=[], # tool names to include (matched against MCPTool.name); empty = no filter
ord_ids=[], # ORD IDs to include (extracted from fragment URL for LoB, or matched
# against IntegrationDependency.ord_id for customer agents); empty = no filter
)
```

Both fields default to empty lists. `names` is applied after fetching; `ord_ids` is applied before fetching, skipping non-matching fragments.

> Both filter classes use AND semantics: if both fields are set, a result must match all of them to be included.

### Data Models

Expand Down
Loading
Loading