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.42.0"
version = "0.43.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 @@ -54,6 +54,7 @@

from sap_cloud_sdk.agentgateway._models import (
AuthResult,
ConnectedSystem,
MCPTool,
MCPToolFilter,
Agent,
Expand All @@ -78,6 +79,7 @@
"ClientConfig",
# Data models
"AuthResult",
"ConnectedSystem",
"MCPTool",
"MCPToolFilter",
"Agent",
Expand Down
112 changes: 97 additions & 15 deletions src/sap_cloud_sdk/agentgateway/_fragments.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,59 +4,72 @@
- Label constants for managed-runtime fragment types
- Fragment listing by label (MCP, A2A, IAS)
- IAS fragment name lookup for auth flows
- Active integration listing for tenant context
"""

import logging
from enum import Enum

from sap_cloud_sdk.destination import (
create_fragment_client,
Label,
ListOptions,
)
from sap_cloud_sdk.destination._models import Level

from sap_cloud_sdk.agentgateway.exceptions import MCPServerNotFoundError
from sap_cloud_sdk.agentgateway._models import ConnectedSystem, FragmentLabel
from sap_cloud_sdk.core.telemetry import Module

logger = logging.getLogger(__name__)

# Shared label key for all managed-runtime fragment types
LABEL_KEY = "sap-managed-runtime-type"

_DESTINATION_INSTANCE = "default"


class FragmentLabel(str, Enum):
"""Label values for the sap-managed-runtime-type fragment label key."""
# Label keys for integration metadata stored on system fragments
_LABEL_GTID = "sap-managed-runtime-gtid"
Comment thread
I542102 marked this conversation as resolved.
_LABEL_ORD_ID = "sap-managed-runtime-ordid"
_LABEL_SYSTEM_TYPE = "sap-managed-runtime-system-type"

MCP = "agw.mcp.server"
A2A = "agw.a2a.server"
IAS = "subscriber.ias"
IAS_USER = "subscriber.ias.user"
_DESTINATION_INSTANCE = "default"


def _list_fragments_by_label(label: FragmentLabel, tenant_subdomain: str) -> list:
def _list_fragments_by_label(
label: FragmentLabel,
tenant_subdomain: str,
gtids: list[str] | None = None,
) -> list:
filter_labels = [Label(key=LABEL_KEY, values=[label.value])]
if gtids:
filter_labels.append(Label(key=_LABEL_GTID, values=gtids))
client = create_fragment_client(
instance=_DESTINATION_INSTANCE,
_telemetry_source=Module.AGENTGATEWAY,
)
return client.list_instance_fragments(
filter=ListOptions(filter_labels=[Label(key=LABEL_KEY, values=[label.value])]),
filter=ListOptions(filter_labels=filter_labels),
tenant=tenant_subdomain,
)


def list_mcp_fragments(tenant_subdomain: str) -> list:
def list_mcp_fragments(
tenant_subdomain: str,
gtids: list[str] | None = None,
) -> list:
"""List destination fragments with MCP server label.

Args:
tenant_subdomain: Tenant subdomain for multi-tenant lookup.
gtids: Optional list of global tenant IDs of integrated
systems to filter by. When set, only fragments whose
``sap-managed-runtime-gtid`` label matches one of these values are
returned (filter is applied server-side by the Destination Service).

Returns:
List of fragments with sap-managed-runtime-type=agw.mcp.server label.
List of fragments with sap-managed-runtime-type=agw.mcp.server label
(and, if provided, matching one of the requested global tenant IDs).
"""
logger.debug("Fetching MCP fragments for tenant '%s'", tenant_subdomain)
return _list_fragments_by_label(FragmentLabel.MCP, tenant_subdomain)
return _list_fragments_by_label(FragmentLabel.MCP, tenant_subdomain, gtids)


def list_a2a_fragments(tenant_subdomain: str) -> list:
Expand Down Expand Up @@ -118,3 +131,72 @@ def get_ias_user_fragment_name(tenant_subdomain: str) -> str:
f"for tenant '{tenant_subdomain}'"
)
return fragments[0].name


def _list_active_integrations(tenant_subdomain: str) -> list[ConnectedSystem]:
"""List all active backend system integrations for the given tenant.

Reads Destination Service instance fragments to discover active backend
system integrations for the given tenant. Each fragment represents a
connected backend system (e.g. SAP PCE, SAP S/4HANA).

Retrieves integration metadata from fragment labels:
- sap-managed-runtime-gtid: GTID of the connected partner system.
- sap-managed-runtime-system-type: Application namespace (e.g. "sap.pce").
- sap-managed-runtime-ordid: Sanitized ORD ID of the integration dependency.

Args:
tenant_subdomain: Subscriber tenant subdomain.

Returns:
List of ConnectedSystem dicts, each with keys:
- global_tenant_id: GTID of the connected partner system.
- system_type: Application namespace of the partner (e.g. "sap.pce").
- integration_dependency: ORD ID of the integration dependency fulfilled.
Returns empty list if no active integrations exist.
"""
client = create_fragment_client(
instance=_DESTINATION_INSTANCE,
_telemetry_source=Module.AGENTGATEWAY,
)
fragments = client.list_instance_fragments(
filter=ListOptions(
filter_labels=[
Label(
key=LABEL_KEY,
values=[FragmentLabel.MCP.value],
)
]
),
tenant=tenant_subdomain,
)

result: list[ConnectedSystem] = []
for fragment in fragments:
labels = {
lbl.key: lbl.values[0] if lbl.values else None
for lbl in client.get_fragment_labels(
name=fragment.name,
level=Level.SERVICE_INSTANCE,
tenant=tenant_subdomain,
)
}
gtid = labels.get(_LABEL_GTID)
system_type = labels.get(_LABEL_SYSTEM_TYPE)
ord_id = labels.get(_LABEL_ORD_ID)

if not system_type:
logger.debug(
"Fragment '%s' is missing system_type label; system_type will be None in result",
fragment.name,
)

result.append(
ConnectedSystem(
global_tenant_id=gtid,
system_type=system_type,
integration_dependency=ord_id,
)
)

return result
13 changes: 9 additions & 4 deletions src/sap_cloud_sdk/agentgateway/_lob.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@

from sap_cloud_sdk.agentgateway._fragments import (
LABEL_KEY,
FragmentLabel,
get_ias_fragment_name,
get_ias_user_fragment_name,
list_mcp_fragments,
Expand All @@ -32,6 +31,7 @@
Agent,
AgentCard,
AgentCardFilter,
FragmentLabel,
MCPTool,
MCPToolFilter,
)
Expand Down Expand Up @@ -378,8 +378,11 @@ 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.
filter: Optional MCPToolFilter narrowing results by tool name, ORD ID,
or GTID. If None or empty, all tools are included.
``gtids`` filters fragments server-side via the Destination Service.
``ord_ids`` filters before fetching.
``names`` filters after fetching.

Returns:
List of MCPTool objects from all MCP servers.
Expand All @@ -390,7 +393,9 @@ async def get_mcp_tools_lob(

logger.info("Listing MCP fragments for tenant '%s'", tenant_subdomain)

fragments = await loop.run_in_executor(None, list_mcp_fragments, tenant_subdomain)
fragments = await loop.run_in_executor(
None, list_mcp_fragments, tenant_subdomain, f.gtids or None
)

if not fragments:
logger.debug(
Expand Down
32 changes: 31 additions & 1 deletion src/sap_cloud_sdk/agentgateway/_models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,32 @@
"""Data models for Agent Gateway MCP tools."""

from dataclasses import dataclass, field
from typing import Any
from enum import Enum
from typing import Any, Optional, TypedDict


class FragmentLabel(str, Enum):
"""Label values for the sap-managed-runtime-type fragment label key."""

MCP = "agw.mcp.server"
A2A = "agw.a2a.server"
IAS = "subscriber.ias"
IAS_USER = "subscriber.ias.user"


class ConnectedSystem(TypedDict):
"""Metadata for a connected backend system integration.

Attributes:
global_tenant_id: GTID of the connected partner system.
system_type: Application namespace of the partner (e.g. ``"sap.pce"``).
May be ``None`` for older integrations missing the label.
integration_dependency: ORD ID of the integration dependency fulfilled.
"""

global_tenant_id: Optional[str]
system_type: Optional[str]
integration_dependency: Optional[str]


@dataclass
Expand Down Expand Up @@ -169,6 +194,9 @@ class MCPToolFilter:
agents, or matched against IntegrationDependency.ord_id for
customer agents). Applied before fetching, skipping non-matching
fragments.
gtids: Global tenant IDs of the connected systems whose tools should be
listed. Only supported in the LoB flow; the Destination Service
filters fragments server-side. Ignored by the customer flow.

Example:
```python
Expand All @@ -178,10 +206,12 @@ class MCPToolFilter:
filter=MCPToolFilter(
names=["get-sales-order"],
ord_ids=["sap.s4:apiAccess:salesOrder:v1"],
gtids=["9e88a0c4-ab32-46d8-b1d3-07cbcac11831"],
)
)
```
"""

names: list[str] = field(default_factory=list)
ord_ids: list[str] = field(default_factory=list)
gtids: list[str] = field(default_factory=list)
55 changes: 53 additions & 2 deletions src/sap_cloud_sdk/agentgateway/agw_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@
Agent,
AgentCardFilter,
AuthResult,
ConnectedSystem,
MCPTool,
MCPToolFilter,
)
from sap_cloud_sdk.agentgateway._token_cache import _GatewayUrlCache, _TokenCache
from sap_cloud_sdk.agentgateway.exceptions import AgentGatewaySDKError
from sap_cloud_sdk.agentgateway import _fragments
from sap_cloud_sdk.core.telemetry import Module, Operation, record_metrics

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -376,8 +378,9 @@ async def list_mcp_tools(
user_token: User's JWT for principal propagation.
Can be a string or a callable returning a string.
If provided, uses user-scoped auth instead of system auth.
filter: Optional filter to narrow results by tool name or ORD ID.
If None or empty, all tools are included.
filter: Optional filter to narrow results by tool name, ORD ID, or
global tenant ID. If None or empty, all tools are included.
See :class:`MCPToolFilter` for supported fields.

Returns:
List of MCPTool objects from all MCP servers.
Expand All @@ -400,6 +403,7 @@ async def list_mcp_tools(
filter=MCPToolFilter(
names=["get-sales-order"],
ord_ids=["sap.s4:apiAccess:salesOrder:v1"],
gtids=["<gtid>"],
)
)
```
Expand Down Expand Up @@ -519,6 +523,53 @@ async def list_agent_cards(
logger.exception("Unexpected error during agent card discovery")
raise AgentGatewaySDKError(f"Agent card discovery failed: {e}") from e

@record_metrics(
Module.AGENTGATEWAY, Operation.AGENTGATEWAY_LIST_ACTIVE_INTEGRATIONS
)
def list_active_integrations(self) -> list[ConnectedSystem]:
"""List all active backend system integrations for the current tenant.

Returns the connected backend systems (e.g. SAP PCE, SAP S/4HANA) that
are currently active for this tenant. Use this to determine which systems
are connected and which GTIDs to pass when loading MCP tools.

Only available for LoB agents. Customer agents should use the
``integrationDependencies`` field in their credentials file instead.

Requires tenant_subdomain to be configured on the client.

Returns:
List of ConnectedSystem dicts, each with:
- global_tenant_id: GTID of the connected partner system.
- system_type: Application namespace (e.g. "sap.pce", "sap.s4").
- integration_dependency: ORD ID fulfilled by this integration.
Returns empty list if no active integrations exist.

Raises:
AgentGatewaySDKError: If tenant_subdomain is not configured, or
if called from a customer agent context.

Example:
```python
integrations = agw_client.list_active_integrations()
gtids = [i["global_tenant_id"] for i in integrations]
tools = await agw_client.list_mcp_tools(
filter=MCPToolFilter(gtids=gtids)
)
```
"""
credentials_path = detect_customer_agent_credentials()
if credentials_path:
raise AgentGatewaySDKError(
"list_active_integrations is not supported for customer agents."
)
if detect_transparent_credentials():
raise AgentGatewaySDKError(
"list_active_integrations is not supported for customer agents."
)
tenant = self._resolve_tenant_subdomain()
return _fragments._list_active_integrations(tenant)
Comment thread
I542102 marked this conversation as resolved.

@record_metrics(Module.AGENTGATEWAY, Operation.AGENTGATEWAY_CALL_MCP_TOOL)
async def call_mcp_tool(
self,
Expand Down
1 change: 1 addition & 0 deletions src/sap_cloud_sdk/core/telemetry/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ class Operation(str, Enum):
AGENTGATEWAY_GET_USER_AUTH = "get_user_auth"
AGENTGATEWAY_LIST_AGENT_CARDS = "list_agent_cards"
AGENTGATEWAY_GET_IAS_CLIENT_ID = "get_ias_client_id"
AGENTGATEWAY_LIST_ACTIVE_INTEGRATIONS = "list_active_integrations"

# Agent Memory Operations
AGENT_MEMORY_ADD_MEMORY = "add_memory"
Expand Down
Loading
Loading