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
10 changes: 5 additions & 5 deletions src/sap_cloud_sdk/agentgateway/_customer.py
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,7 @@ async def _list_server_tools(
List of MCPTool objects from this server.

Raises:
AgentGatewaySDKError: If server does not provide serverInfo.name.
AgentGatewaySDKError: If server does not provide server_info.name.
"""
async with httpx.AsyncClient(
headers={
Expand All @@ -650,15 +650,15 @@ async def _list_server_tools(

if not (
init_result
and init_result.serverInfo
and init_result.serverInfo.name
and init_result.server_info
and init_result.server_info.name
):
raise AgentGatewaySDKError(
f"MCP server at '{url}' did not provide serverInfo.name. "
f"MCP server at '{url}' did not provide server_info.name. "
"This is required by the MCP protocol."
)

server_name = init_result.serverInfo.name
server_name = init_result.server_info.name
result = await session.list_tools()

return [
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
19 changes: 12 additions & 7 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 @@ -344,10 +344,10 @@ async def list_server_tools(
async with ClientSession(read, write) as session:
init_result = await session.initialize()
server_name = (
init_result.serverInfo.name
init_result.server_info.name
if init_result
and init_result.serverInfo
and init_result.serverInfo.name
and init_result.server_info
and init_result.server_info.name
else fragment_name
)
result = await session.list_tools()
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
34 changes: 32 additions & 2 deletions 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 @@ -40,7 +65,7 @@ class MCPTool:

Attributes:
name: Tool name on MCP server (used when calling the tool)
server_name: MCP server name from serverInfo.name
server_name: MCP server name from server_info.name
description: Tool description
input_schema: JSON schema for tool input parameters
url: MCP endpoint URL
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)
Loading