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: 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
lentzvitoria 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
34 changes: 32 additions & 2 deletions src/sap_cloud_sdk/agentgateway/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,32 @@

import json
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 @@ -41,7 +66,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 @@ -197,6 +222,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 @@ -206,10 +234,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,12 +34,14 @@
Agent,
AgentCardFilter,
AuthResult,
ConnectedSystem,
MCPTool,
MCPToolFilter,
)
from sap_cloud_sdk.core._tenant import _validate_tenant_subdomain
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 @@ -393,8 +395,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 @@ -417,6 +420,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 @@ -536,6 +540,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
lentzvitoria 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 @@ -196,6 +196,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"

# DPI NG Consent Operations
DPI_NG_CONSENT_CREATE_CLIENT = "consent_create_client"
Expand Down
32 changes: 32 additions & 0 deletions tests/agentgateway/integration/connected_systems.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
Feature: Agent Gateway Connected Systems Integration
As a LoB agent developer using the SDK
I want to discover which backend systems are connected for my tenant
So that I can scope MCP tool discovery to specific systems

Background:
Given the Agent Gateway client is available

Scenario: List connected systems returns a list
When I call list_active_integrations
Then the result should be a list of ConnectedSystem

Scenario: Each connected system has required fields
When I call list_active_integrations
Then each connected system should have a non-empty global_tenant_id
And each connected system should have a non-null integration_dependency or system_type

Scenario: Connected systems with missing labels are still returned
When I call list_active_integrations
Then fragments with missing labels should appear with None values

Scenario: List MCP tools filtered by GTID from connected system
When I call list_active_integrations
And at least one connected system is present
When I call list_mcp_tools filtered by the first connected system gtid
Then the result should be a list of MCPTool

Scenario: Customer agent cannot call list_active_integrations
Given a customer agent client is configured
When I call list_active_integrations with the customer agent client
Then the operation should fail with AgentGatewaySDKError
And the error message should mention "not supported for customer agents"
Loading