From a02365eb646b801fec5b0fe0e6e3f12e11fd3e4d Mon Sep 17 00:00:00 2001 From: I536049 Date: Thu, 21 May 2026 11:08:32 -0700 Subject: [PATCH] feat(extensibility): clean up ORD integration --- pyproject.toml | 2 +- src/sap_cloud_sdk/extensibility/__init__.py | 5 - .../extensibility/_ord_integration.py | 229 ------------ src/sap_cloud_sdk/extensibility/user-guide.md | 107 ------ .../unit/test_ord_integration.py | 340 ------------------ 5 files changed, 1 insertion(+), 682 deletions(-) delete mode 100644 src/sap_cloud_sdk/extensibility/_ord_integration.py delete mode 100644 tests/extensibility/unit/test_ord_integration.py diff --git a/pyproject.toml b/pyproject.toml index e6cae92..c57ff76 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sap-cloud-sdk" -version = "0.19.3" +version = "0.20.0" description = "SAP Cloud SDK for Python" readme = "README.md" license = "Apache-2.0" diff --git a/src/sap_cloud_sdk/extensibility/__init__.py b/src/sap_cloud_sdk/extensibility/__init__.py index 3454f95..3d89622 100644 --- a/src/sap_cloud_sdk/extensibility/__init__.py +++ b/src/sap_cloud_sdk/extensibility/__init__.py @@ -89,9 +89,6 @@ def _mock_file(name: str) -> str: Tools, ) from sap_cloud_sdk.extensibility._noop_transport import NoOpTransport -from sap_cloud_sdk.extensibility._ord_integration import ( - add_extension_integration_dependencies, -) from sap_cloud_sdk.extensibility._ums_transport import UmsTransport from sap_cloud_sdk.extensibility.client import ExtensibilityClient from sap_cloud_sdk.extensibility.config import ExtensibilityConfig, HookConfig @@ -187,8 +184,6 @@ def create_client( # A2A card helpers "build_extension_capabilities", "EXTENSION_CAPABILITY_SCHEMA_VERSION", - # ORD integration - "add_extension_integration_dependencies", # Models -- A2A card "ExtensionCapability", "ToolAdditions", diff --git a/src/sap_cloud_sdk/extensibility/_ord_integration.py b/src/sap_cloud_sdk/extensibility/_ord_integration.py deleted file mode 100644 index 2074b6b..0000000 --- a/src/sap_cloud_sdk/extensibility/_ord_integration.py +++ /dev/null @@ -1,229 +0,0 @@ -"""ORD integration helpers for extensibility. - -Provides utilities to inject extension capability MCP servers into the -ORD (Open Resource Discovery) document at runtime. -""" - -from __future__ import annotations - -import logging -from datetime import datetime, timezone -from typing import Optional - -from sap_cloud_sdk.extensibility.client import ExtensibilityClient -from sap_cloud_sdk.extensibility._models import ExtensionCapabilityImplementation - -logger = logging.getLogger(__name__) - - -def _derive_mcp_name_from_ord_id(ord_id: str) -> str: - """Derive a readable MCP server name from its ordId. - - ordId format: {namespace}:apiResource:{resource-name}:v1 - - Returns the namespace short name (first segment before any dot) in title case. - Example: "sap.s4:apiResource:s4bpintelmcp:v1" -> "S4" - - Args: - ord_id: The MCP server ordId. - - Returns: - A readable name derived from the ordId. - """ - namespace = ord_id.split(":")[0] if ":" in ord_id else ord_id - namespace_short = namespace.split(".")[-1] if namespace else "unknown" - return namespace_short.replace("_", " ").replace("-", " ").title() - - -def _map_capability_to_integration_dependencies( - capability_impl: ExtensionCapabilityImplementation, - agent: Optional[dict] = None, - base_integration_deps: Optional[list[dict]] = None, -) -> list[dict]: - """Map a capability implementation to ORD IntegrationDependency structure. - - Args: - capability_impl: ExtensionCapabilityImplementation from the extensibility client. - agent: The agent dict from the ORD document to derive namespace and partOfPackage. - base_integration_deps: List of existing document-level integration dependencies - to check against for duplicate MCP servers. - - Returns: - List of IntegrationDependency dicts ready for ORD injection. - """ - mcp_servers = capability_impl.mcp_servers if capability_impl else [] - if not mcp_servers: - return [] - - base_api_resource_ord_ids: set = set() - if base_integration_deps: - for base_dep in base_integration_deps: - for aspect in base_dep.get("aspects", []): - for api_resource in aspect.get("apiResources", []): - base_api_resource_ord_ids.add(api_resource["ordId"]) - - current_time = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") - - namespace = "" - part_of_package = None - if agent: - agent_ord_id = agent.get("ordId", "") - namespace = agent_ord_id.split(":")[0] if agent_ord_id else "" - part_of_package = agent.get("partOfPackage") - - aspects = [] - for mcp_server in mcp_servers: - if mcp_server.ord_id in base_api_resource_ord_ids: - logger.debug( - "Skipping MCP %s - already in base integration dependencies", - mcp_server.ord_id, - ) - continue - - mcp_name = _derive_mcp_name_from_ord_id(mcp_server.ord_id) - aspect = { - "title": f"{mcp_name} Extension MCP", - "mandatory": False, - "apiResources": [ - { - "ordId": mcp_server.ord_id, - } - ], - } - aspects.append(aspect) - - if not aspects: - logger.info( - "All extended MCP servers already present in base integration dependencies" - ) - return [] - - integration_dependency = { - "ordId": f"{namespace}:integrationDependency:extension-mcp:v1", - "title": "Extension MCP Servers", - "version": "1.0.0", - "releaseStatus": "active", - "visibility": "public", - "mandatory": False, - "partOfPackage": part_of_package, - "lastUpdate": current_time, - "aspects": aspects, - } - - return [integration_dependency] - - -def fetch_extension_integration_dependencies( - ext_client: ExtensibilityClient, - capability_id: str = "default", - agent: Optional[dict] = None, - base_integration_deps: Optional[list[dict]] = None, - tenant: Optional[str] = None, -) -> list[dict]: - """Fetch extension capability implementation and map to IntegrationDependencies. - - This function: - 1. Calls ext_client.get_extension_capability_implementation() - 2. Maps the response to ORD IntegrationDependency structure - 3. Returns list of IntegrationDependencies to inject into ORD document - - Args: - ext_client: The extensibility client. - capability_id: The capability ID to fetch (default: "default") - agent: The agent dict from the ORD document to derive namespace and partOfPackage - base_integration_deps: List of existing document-level integration dependencies - to check against for duplicate MCP servers - tenant: Tenant ID for the extensibility service request. - - Returns: - List of IntegrationDependency dicts ready for ORD injection. - """ - try: - capability_impl = ext_client.get_extension_capability_implementation( - capability_id=capability_id, - skip_cache=True, - tenant=tenant or "", - ) - - if not capability_impl: - logger.info( - "No extension capability implementation found for capability_id=%s", - capability_id, - ) - return [] - - return _map_capability_to_integration_dependencies( - capability_impl, agent, base_integration_deps - ) - - except Exception as e: - logger.error("Failed to fetch extension capabilities: %s", e) - raise - - -def add_extension_integration_dependencies( - document: dict, - local_tenant_id: Optional[str] = None, - ext_client: Optional[ExtensibilityClient] = None, -) -> None: - """Add extension integration dependencies to the ORD document. - - This method: - 1. Gets the agent from the document - 2. Fetches extension capability implementation via ext_client - 3. Injects full IntegrationDependency objects at document level - 4. Injects ordId references at agent level - - Note: - This method should only be used for the system-instance ORD document. - It is not intended for use with other ORD documents. - - Args: - document: The ORD document dict (modified in-place) - local_tenant_id: Optional tenant ID for fetching tenant-specific extensions - ext_client: Optional extensibility client. If not provided, no extension - dependencies will be added. - """ - if ext_client is None: - logger.debug( - "No extensibility client provided, skipping extension integration dependencies" - ) - return - - try: - agent = document.get("agents", [{}])[0] if document.get("agents") else None - - base_integration_deps = document.get("integrationDependencies", []) - - ext_integration_deps = fetch_extension_integration_dependencies( - ext_client=ext_client, - agent=agent, - base_integration_deps=base_integration_deps, - tenant=local_tenant_id, - ) - - if not ext_integration_deps: - return - - if "integrationDependencies" not in document: - document["integrationDependencies"] = [] - document["integrationDependencies"].extend(ext_integration_deps) - - if agent: - if "integrationDependencies" not in agent: - agent["integrationDependencies"] = [] - for ext_dep in ext_integration_deps: - ext_ord_id = ext_dep["ordId"] - if ext_ord_id not in agent["integrationDependencies"]: - agent["integrationDependencies"].append(ext_ord_id) - - logger.info( - "Added %d extension integration dependencies to instance ORD", - len(ext_integration_deps), - ) - - except Exception as e: - logger.warning( - "Failed to fetch extension capabilities, continuing without them: %s", - e, - ) diff --git a/src/sap_cloud_sdk/extensibility/user-guide.md b/src/sap_cloud_sdk/extensibility/user-guide.md index 043e539..c920148 100644 --- a/src/sap_cloud_sdk/extensibility/user-guide.md +++ b/src/sap_cloud_sdk/extensibility/user-guide.md @@ -809,113 +809,6 @@ The JSON file uses the same schema as the extensibility backend response: See `src/sap_cloud_sdk/extensibility/local_extensibility_example.json` for a ready-to-use template. -## ORD Document Integration - -> **Note**: This feature should only be used for the system-instance ORD document. It is not intended for use with other ORD documents. - -The `add_extension_integration_dependencies()` function injects extension capability MCP servers into the ORD (Open Resource Discovery) document at runtime. This enables the agent to dynamically expose MCP servers from the extensibility service in its ORD document for discovery. - -### Import - -```python -from sap_cloud_sdk.extensibility import ( - add_extension_integration_dependencies, - create_client, -) -``` - -### Usage - -```python -from sap_cloud_sdk.extensibility import ( - add_extension_integration_dependencies, - create_client, -) - -# Create an extensibility client -ext_client = create_client("sap.ai:agent:myAgent:v1") - -# Load your ORD document (system-instance) -document = load_ord_document(ORD_SYSTEM_INSTANCE_PATH) - -# Inject extension integration dependencies -add_extension_integration_dependencies( - document=document, - local_tenant_id=local_tenant_id, - ext_client=ext_client, -) -``` - -### Parameters - -- `document`: The ORD document dict. Modified in-place. -- `local_tenant_id`: Optional tenant ID for fetching tenant-specific extensions. -- `ext_client`: The extensibility client. If not provided, no extension dependencies will be added. - -### What It Does - -1. Fetches the extension capability implementation from the extensibility service via the client -2. Checks each MCP server against existing document-level integration dependencies -3. Filters out MCP servers that are already present in base integration dependencies -4. Creates a new IntegrationDependency with aspects for only the new MCP servers -5. Injects the IntegrationDependency at document level (full object) -6. Injects the IntegrationDependency ordId reference at agent level (string array) - -### Duplicate Filtering - -The function checks if an MCP server's ordId already exists in any base integration dependency's aspects. If it exists, that MCP server is skipped to avoid duplication. - -If ALL extended MCP servers are already present in base integration dependencies, no extension integration dependency is created. - -### Example: Full ORD Endpoint Integration - -```python -from sap_cloud_sdk.extensibility import ( - add_extension_integration_dependencies, - create_client, -) -import json -from pathlib import Path - -ORD_SYSTEM_INSTANCE_PATH = Path(__file__).parent / "document-system-instance.json" - - -def load_ord_document(path: Path) -> dict: - with open(path, "r", encoding="utf-8") as f: - return json.load(f) - - -async def ord_document_system_instance(request: Request) -> JSONResponse: - local_tenant_id = request.query_params.get("local-tenant-id", "") - - document = load_ord_document(ORD_SYSTEM_INSTANCE_PATH) - - # Replace tenant placeholder - doc_str = json.dumps(document) - doc_str = doc_str.replace("{{LOCAL_TENANT_ID}}", local_tenant_id) - document = json.loads(doc_str) - - # Create extensibility client - ext_client = create_client("sap.ai:agent:myAgent:v1") - - # Add extension integration dependencies - add_extension_integration_dependencies( - document=document, - local_tenant_id=local_tenant_id, - ext_client=ext_client, - ) - - return JSONResponse(content=document) -``` - -### Important Notes - -- This function modifies the `document` dict in-place. -- If an MCP server is already defined in a base integration dependency, it will not be duplicated in the extension integration dependency. -- If all MCP servers from extensibility are duplicates, the function returns without adding anything. -- On failure (e.g., extensibility service unavailable), the function logs a warning and continues without adding extension dependencies. The agent continues with its base ORD document. -- The `lastUpdate` field is set to the current timestamp to trigger re-aggregation. - ## Notes - Create one `ExtensibilityClient` (via `create_client()`) and reuse it for multiple capability lookups where appropriate. diff --git a/tests/extensibility/unit/test_ord_integration.py b/tests/extensibility/unit/test_ord_integration.py deleted file mode 100644 index 8da1564..0000000 --- a/tests/extensibility/unit/test_ord_integration.py +++ /dev/null @@ -1,340 +0,0 @@ -"""Tests for ORD integration helpers.""" - -from unittest.mock import MagicMock - - -from sap_cloud_sdk.extensibility._ord_integration import ( - _derive_mcp_name_from_ord_id, - _map_capability_to_integration_dependencies, - add_extension_integration_dependencies, -) -from sap_cloud_sdk.extensibility._models import ( - ExtensionCapabilityImplementation, - McpServer, -) - - -class TestDeriveMcpNameFromOrdId: - """Tests for _derive_mcp_name_from_ord_id.""" - - def test_sap_s4_namespace(self): - assert ( - _derive_mcp_name_from_ord_id("sap.s4:apiResource:s4bpintelmcp:v1") == "S4" - ) - - def test_sap_ariba_namespace(self): - assert ( - _derive_mcp_name_from_ord_id("sap.ariba:apiResource:hardwareMcp:v1") - == "Ariba" - ) - - def test_simple_namespace(self): - assert ( - _derive_mcp_name_from_ord_id("sap.mcp:apiResource:serviceNow:v1") == "Mcp" - ) - - def test_multiple_dots_namespace(self): - assert ( - _derive_mcp_name_from_ord_id( - "sap.btpn8n:apiResource:ManagedN8nMcpServer:v1" - ) - == "Btpn8N" - ) - - def test_preserves_full_namespace(self): - result = _derive_mcp_name_from_ord_id("sap.custom:apiResource:customMcp:v1") - assert result == "Custom" - - -class TestMapCapabilityToIntegrationDependencies: - """Tests for _map_capability_to_integration_dependencies.""" - - def test_no_mcp_servers_returns_empty(self): - capability_impl = ExtensionCapabilityImplementation( - capability_id="default", - mcp_servers=[], - ) - result = _map_capability_to_integration_dependencies(capability_impl) - assert result == [] - - def test_with_mcp_servers_no_base_deps(self): - capability_impl = ExtensionCapabilityImplementation( - capability_id="default", - mcp_servers=[ - McpServer( - ord_id="sap.s4:apiResource:s4bpintelmcp:v1", - global_tenant_id="tenant-s4-1", - ), - ], - ) - agent = { - "ordId": "sap.agtpocext:agent:extensibility-agent:v1", - "partOfPackage": "sap.agtpocext:package:test:v1", - } - - result = _map_capability_to_integration_dependencies( - capability_impl, agent=agent - ) - - assert len(result) == 1 - dep = result[0] - assert dep["ordId"] == "sap.agtpocext:integrationDependency:extension-mcp:v1" - assert dep["title"] == "Extension MCP Servers" - assert dep["version"] == "1.0.0" - assert dep["releaseStatus"] == "active" - assert dep["visibility"] == "public" - assert dep["mandatory"] is False - assert dep["partOfPackage"] == "sap.agtpocext:package:test:v1" - assert "lastUpdate" in dep - assert len(dep["aspects"]) == 1 - assert dep["aspects"][0]["title"] == "S4 Extension MCP" - assert dep["aspects"][0]["mandatory"] is False - assert ( - dep["aspects"][0]["apiResources"][0]["ordId"] - == "sap.s4:apiResource:s4bpintelmcp:v1" - ) - - def test_with_duplicate_mcps_filtered(self): - """MCP servers already in base are filtered out.""" - capability_impl = ExtensionCapabilityImplementation( - capability_id="default", - mcp_servers=[ - McpServer( - ord_id="sap.s4:apiResource:s4bpintelmcp:v1", - global_tenant_id="tenant-s4-1", - ), - McpServer( - ord_id="sap.btpn8n:apiResource:ManagedN8nMcpServer:v1", - global_tenant_id="tenant-n8n-1", - ), - ], - ) - agent = { - "ordId": "sap.agtpocext:agent:extensibility-agent:v1", - "partOfPackage": "sap.agtpocext:package:test:v1", - } - - base_integration_deps = [ - { - "ordId": "sap.agtpocext:integrationDependency:n8n-mcp:v1", - "aspects": [ - { - "title": "N8n MCP", - "apiResources": [ - {"ordId": "sap.btpn8n:apiResource:ManagedN8nMcpServer:v1"} - ], - } - ], - } - ] - - result = _map_capability_to_integration_dependencies( - capability_impl, agent=agent, base_integration_deps=base_integration_deps - ) - - assert len(result) == 1 - dep = result[0] - assert len(dep["aspects"]) == 1 - assert ( - dep["aspects"][0]["apiResources"][0]["ordId"] - == "sap.s4:apiResource:s4bpintelmcp:v1" - ) - - def test_all_mcps_duplicate_returns_empty(self): - """All MCP servers are duplicates, returns empty list.""" - capability_impl = ExtensionCapabilityImplementation( - capability_id="default", - mcp_servers=[ - McpServer( - ord_id="sap.btpn8n:apiResource:ManagedN8nMcpServer:v1", - global_tenant_id="tenant-n8n-1", - ), - ], - ) - agent = { - "ordId": "sap.agtpocext:agent:extensibility-agent:v1", - "partOfPackage": "sap.agtpocext:package:test:v1", - } - - base_integration_deps = [ - { - "ordId": "sap.agtpocext:integrationDependency:n8n-mcp:v1", - "aspects": [ - { - "title": "N8n MCP", - "apiResources": [ - {"ordId": "sap.btpn8n:apiResource:ManagedN8nMcpServer:v1"} - ], - } - ], - } - ] - - result = _map_capability_to_integration_dependencies( - capability_impl, agent=agent, base_integration_deps=base_integration_deps - ) - - assert result == [] - - def test_multiple_mcps_all_new(self): - """Multiple new MCP servers, none duplicating base.""" - capability_impl = ExtensionCapabilityImplementation( - capability_id="default", - mcp_servers=[ - McpServer( - ord_id="sap.s4:apiResource:s4bpintelmcp:v1", - global_tenant_id="tenant-s4-1", - ), - McpServer( - ord_id="sap.ariba:apiResource:hardwareMcp:v1", - global_tenant_id="tenant-ariba-1", - ), - ], - ) - agent = { - "ordId": "sap.agtpocext:agent:extensibility-agent:v1", - "partOfPackage": "sap.agtpocext:package:test:v1", - } - - result = _map_capability_to_integration_dependencies( - capability_impl, agent=agent - ) - - assert len(result) == 1 - dep = result[0] - assert len(dep["aspects"]) == 2 - aspect_titles = [a["title"] for a in dep["aspects"]] - assert "S4 Extension MCP" in aspect_titles - assert "Ariba Extension MCP" in aspect_titles - - -class TestAddExtensionIntegrationDependencies: - """Tests for add_extension_integration_dependencies.""" - - def test_no_ext_client_returns_early(self): - """When ext_client is None, returns early without modifying document.""" - document = { - "agents": [{"ordId": "sap.agtpocext:agent:extensibility-agent:v1"}], - "integrationDependencies": [], - } - - add_extension_integration_dependencies(document=document, ext_client=None) - - assert document["integrationDependencies"] == [] - - def test_with_ext_client_adds_integration_deps(self): - """With ext_client, adds integration dependencies to document.""" - mock_client = MagicMock() - mock_client.get_extension_capability_implementation.return_value = ( - ExtensionCapabilityImplementation( - capability_id="default", - mcp_servers=[ - McpServer( - ord_id="sap.s4:apiResource:s4bpintelmcp:v1", - global_tenant_id="tenant-s4-1", - ), - ], - ) - ) - - document = { - "agents": [ - { - "ordId": "sap.agtpocext:agent:extensibility-agent:v1", - "partOfPackage": "sap.agtpocext:package:test:v1", - "integrationDependencies": [], - } - ], - "integrationDependencies": [], - } - - add_extension_integration_dependencies( - document=document, - local_tenant_id="tenant-1", - ext_client=mock_client, - ) - - assert len(document["integrationDependencies"]) == 1 - dep = document["integrationDependencies"][0] - assert dep["ordId"] == "sap.agtpocext:integrationDependency:extension-mcp:v1" - assert len(dep["aspects"]) == 1 - - agent = document["agents"][0] - assert ( - "sap.agtpocext:integrationDependency:extension-mcp:v1" - in agent["integrationDependencies"] - ) - - def test_existing_base_integration_deps_preserved(self): - """Existing base integration dependencies are preserved.""" - mock_client = MagicMock() - mock_client.get_extension_capability_implementation.return_value = ( - ExtensionCapabilityImplementation( - capability_id="default", - mcp_servers=[ - McpServer( - ord_id="sap.s4:apiResource:s4bpintelmcp:v1", - global_tenant_id="tenant-s4-1", - ), - ], - ) - ) - - document = { - "agents": [ - { - "ordId": "sap.agtpocext:agent:extensibility-agent:v1", - "partOfPackage": "sap.agtpocext:package:test:v1", - "integrationDependencies": [], - } - ], - "integrationDependencies": [ - { - "ordId": "sap.agtpocext:integrationDependency:n8n-mcp:v1", - "title": "N8N MCP Servers", - "aspects": [ - { - "title": "N8n MCP", - "apiResources": [ - { - "ordId": "sap.btpn8n:apiResource:ManagedN8nMcpServer:v1" - } - ], - } - ], - } - ], - } - - add_extension_integration_dependencies( - document=document, - local_tenant_id="tenant-1", - ext_client=mock_client, - ) - - ord_ids = [dep["ordId"] for dep in document["integrationDependencies"]] - assert "sap.agtpocext:integrationDependency:n8n-mcp:v1" in ord_ids - assert "sap.agtpocext:integrationDependency:extension-mcp:v1" in ord_ids - - def test_empty_capability_returns_early(self): - """When no MCP servers returned, returns early without adding.""" - mock_client = MagicMock() - mock_client.get_extension_capability_implementation.return_value = ( - ExtensionCapabilityImplementation( - capability_id="default", - mcp_servers=[], - ) - ) - - document = { - "agents": [{"ordId": "sap.agtpocext:agent:extensibility-agent:v1"}], - "integrationDependencies": [], - } - - add_extension_integration_dependencies( - document=document, - local_tenant_id="tenant-1", - ext_client=mock_client, - ) - - assert document["integrationDependencies"] == []