Skip to content
Merged
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.19.2"
version = "0.19.3"
description = "SAP Cloud SDK for Python"
readme = "README.md"
license = "Apache-2.0"
Expand Down
26 changes: 13 additions & 13 deletions src/sap_cloud_sdk/core/telemetry/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ def resolve_source_info(
fields when the key is not found in the mapping.

Args:
key: Lookup key in the source mapping (e.g. prefixed tool name or
key: Lookup key in the source mapping (e.g. tool name or
hook ID).
source_mapping: Mapping of keys to source info objects or dicts.
May be ``None``.
Expand Down Expand Up @@ -417,10 +417,8 @@ async def call_extension_tool(
mcp_client: Any,
tool_name: str,
args: dict[str, Any],
extension_name: str,
capability: str = "default",
source_mapping: dict[str, Any] | None = None,
tool_prefix: str = "",
) -> Any:
"""Call an MCP tool with telemetry instrumentation.

Expand All @@ -432,23 +430,25 @@ async def call_extension_tool(
Args:
mcp_client: The MCP client session connected to the tool's server.
Must have an async ``call_tool(name, args)`` method.
tool_name: The raw MCP tool name (before any prefixing).
tool_name: The raw MCP tool name (e.g. ``"create_ticket"``), used
as the lookup key in *source_mapping* and passed directly to
``mcp_client.call_tool()``.
args: Dictionary of arguments to pass to the tool.
extension_name: Human-readable name of the extension. Used as
fallback when *source_mapping* does not contain the tool.
capability: Extension capability ID (default: ``"default"``).
source_mapping: Optional mapping of prefixed tool names to source
info objects (from ``ext_impl.source.tools``).
tool_prefix: The tool prefix (e.g. ``"sap_mcp_servicenow_v1_"``).
Used to reconstruct the lookup key for *source_mapping*.
source_mapping: Optional mapping of tool names to
:class:`~sap_cloud_sdk.extensibility.ExtensionSourceInfo`
objects (from ``ext_impl.source.tools``). Keys must match the
*tool_name* values passed to this function.
See :class:`~sap_cloud_sdk.extensibility.ExtensionSourceMapping`.

Returns:
The tool's response from the MCP server.
"""
lookup_key = tool_prefix + tool_name if tool_prefix else tool_name

See Also:
:func:`call_extension_hook` for hook-based extensions.
"""
resolved_name, resolved_id, resolved_version, resolved_url, resolved_solution_id = (
resolve_source_info(lookup_key, source_mapping, extension_name)
resolve_source_info(tool_name, source_mapping, "unknown")
)

attrs = build_extension_span_attributes(
Expand Down
18 changes: 9 additions & 9 deletions src/sap_cloud_sdk/extensibility/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -489,15 +489,15 @@ class ExtensionSourceMapping:
Returned by the extensibility backend when multiple extensions are merged
into a single capability implementation response.

Tool keys are prefixed tool names
(e.g., ``"sap_mcp_servicenow_v1_create_ticket"``).
Tool keys are raw tool names
(e.g., ``"create_ticket"``).
Hook keys are hook IDs (UUIDs) (e.g.,
``"3f5c8c8a-7b4d-4f9c-a4c0-7d5cb1a39f7e"``).
Values are :class:`ExtensionSourceInfo` objects containing the extension's
name, version, and unique identifier.

Attributes:
tools: Mapping of prefixed tool name to extension source info.
tools: Mapping of tool name to extension source info.
hooks: Mapping of hook ID to extension source info.
"""

Expand All @@ -512,7 +512,7 @@ def from_dict(cls, obj: Dict[str, Any]) -> ExtensionSourceMapping:

{
"tools": {
"sap_mcp_taxvalidator_validate_validate_tax": {
"validate_tax": {
"extensionName": "ap-invoice-extension",
"extensionVersion": "1",
"extensionId": "a1b2c3d4-..."
Expand Down Expand Up @@ -679,7 +679,7 @@ def from_dict(cls, obj: Dict[str, Any]) -> ExtensionCapabilityImplementation:
],
"source": {
"tools": {
"sap_mcp_servicenow_v1_create_ticket": {
"create_ticket": {
"extensionName": "servicenow-ext",
"extensionVersion": "1",
"extensionId": "abc-123"
Expand Down Expand Up @@ -740,8 +740,8 @@ def get_extension_for_tool(self, tool_name: str) -> Optional[str]:
"""Look up the extension name that contributed a specific tool.

Args:
tool_name: The prefixed tool name (e.g.,
``"sap_mcp_servicenow_v1_create_ticket"``).
tool_name: The tool name (e.g.,
``"create_ticket"``).

Returns:
Extension name, or ``None`` if source mapping is not available
Expand Down Expand Up @@ -774,8 +774,8 @@ def get_source_info_for_tool(self, tool_name: str) -> Optional[ExtensionSourceIn
``None`` when source mapping is not available or the tool is not found.

Args:
tool_name: The prefixed tool name (e.g.,
``"sap_mcp_servicenow_v1_create_ticket"``).
tool_name: The tool name (e.g.,
``"create_ticket"``).

Returns:
:class:`ExtensionSourceInfo` for the tool, or ``None``.
Expand Down
5 changes: 3 additions & 2 deletions src/sap_cloud_sdk/extensibility/_ums_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

import httpx

from sap_cloud_sdk.destination import Level
from sap_cloud_sdk.destination import ConsumptionLevel
from sap_cloud_sdk.destination import create_client as create_destination_client
from sap_cloud_sdk.extensibility._models import (
DEFAULT_EXTENSION_CAPABILITY_ID,
Expand Down Expand Up @@ -540,7 +540,8 @@ def get_extension_capability_implementation(
# 1. Resolve destination -----------------------------------------
try:
dest = self._dest_client.get_destination(
self._destination_name, level=Level.SUB_ACCOUNT
self._destination_name,
level=ConsumptionLevel.PROVIDER_SUBACCOUNT,
)
except Exception as exc:
raise TransportError(
Expand Down
34 changes: 22 additions & 12 deletions tests/core/unit/telemetry/test_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -902,7 +902,6 @@ async def _run():
mcp_client=mock_client,
tool_name="create_ticket",
args={"title": "Bug"},
extension_name="ServiceNow",
)
assert result == "result-123"
mock_client.call_tool.assert_awaited_once_with(
Expand All @@ -921,20 +920,32 @@ class FakeSourceInfo:
extension_id: str
extension_version: str

mapping = {"prefix_tool1": FakeSourceInfo("Mapped Ext", "uuid-m", "7")}
mapping = {"create_ticket": FakeSourceInfo("Mapped Ext", "uuid-m", "7")}
mock_client = AsyncMock()
mock_client.call_tool.return_value = "ok"

reset_tool_call_metrics()
result = await call_extension_tool(
mcp_client=mock_client,
tool_name="tool1",
args={},
extension_name="Fallback",
source_mapping=mapping,
tool_prefix="prefix_",
)
assert result == "ok"
with patch(
"sap_cloud_sdk.core.telemetry.extensions._tracer"
) as mock_tracer:
mock_tracer.start_as_current_span = MagicMock(
return_value=MagicMock(
__enter__=MagicMock(), __exit__=MagicMock(return_value=False)
)
)
result = await call_extension_tool(
mcp_client=mock_client,
tool_name="create_ticket",
args={},
source_mapping=mapping,
)
assert result == "ok"
call_args = mock_tracer.start_as_current_span.call_args
attrs = call_args[1]["attributes"]
assert attrs[ATTR_EXTENSION_NAME] == "Mapped Ext"
assert attrs[ATTR_EXTENSION_ID] == "uuid-m"
assert attrs[ATTR_EXTENSION_VERSION] == "7"
assert attrs[ATTR_EXTENSION_ITEM_NAME] == "create_ticket"

asyncio.run(_run())

Expand All @@ -949,7 +960,6 @@ async def _run():
mcp_client=mock_client,
tool_name="t",
args={},
extension_name="E",
)
count, _ = get_tool_call_metrics()
assert count == 1 # Duration still recorded
Expand Down
69 changes: 19 additions & 50 deletions tests/extensibility/unit/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -740,12 +740,12 @@ def test_from_dict_full_new_format(self):
"""Parse a complete source mapping from backend JSON (new format)."""
data = {
"tools": {
"sap_mcp_servicenow_v1_create_ticket": {
"create_ticket": {
"extensionName": "servicenow-ext",
"extensionVersion": "2",
"extensionId": "uuid-sn",
},
"sap_mcp_jira_v1_create_issue": {
"create_issue": {
"extensionName": "jira-ext",
"extensionVersion": "1",
"extensionId": "uuid-jira",
Expand All @@ -765,21 +765,10 @@ def test_from_dict_full_new_format(self):
},
}
mapping = ExtensionSourceMapping.from_dict(data)
assert (
mapping.tools["sap_mcp_servicenow_v1_create_ticket"].extension_name
== "servicenow-ext"
)
assert (
mapping.tools["sap_mcp_servicenow_v1_create_ticket"].extension_version
== "2"
)
assert (
mapping.tools["sap_mcp_servicenow_v1_create_ticket"].extension_id
== "uuid-sn"
)
assert (
mapping.tools["sap_mcp_jira_v1_create_issue"].extension_name == "jira-ext"
)
assert mapping.tools["create_ticket"].extension_name == "servicenow-ext"
assert mapping.tools["create_ticket"].extension_version == "2"
assert mapping.tools["create_ticket"].extension_id == "uuid-sn"
assert mapping.tools["create_issue"].extension_name == "jira-ext"
assert (
mapping.hooks["3f5c8c8a-7b4d-4f9c-a4c0-7d5cb1a39f7e"].extension_name
== "workflow-ext"
Expand All @@ -793,21 +782,16 @@ def test_from_dict_old_format_backward_compat(self):
"""Parse old format where values are plain strings."""
data = {
"tools": {
"sap_mcp_servicenow_v1_create_ticket": "servicenow-ext",
"create_ticket": "servicenow-ext",
},
"hooks": {
"3f5c8c8a-7b4d-4f9c-a4c0-7d5cb1a39f7e": "workflow-ext",
},
}
mapping = ExtensionSourceMapping.from_dict(data)
assert (
mapping.tools["sap_mcp_servicenow_v1_create_ticket"].extension_name
== "servicenow-ext"
)
assert (
mapping.tools["sap_mcp_servicenow_v1_create_ticket"].extension_version == ""
)
assert mapping.tools["sap_mcp_servicenow_v1_create_ticket"].extension_id == ""
assert mapping.tools["create_ticket"].extension_name == "servicenow-ext"
assert mapping.tools["create_ticket"].extension_version == ""
assert mapping.tools["create_ticket"].extension_id == ""
assert (
mapping.hooks["3f5c8c8a-7b4d-4f9c-a4c0-7d5cb1a39f7e"].extension_name
== "workflow-ext"
Expand All @@ -823,15 +807,15 @@ def test_from_dict_only_tools(self):
"""Parse with only tools key present."""
data = {
"tools": {
"prefix_tool": {
"my_tool": {
"extensionName": "my-ext",
"extensionVersion": "1",
"extensionId": "id-1",
}
}
}
mapping = ExtensionSourceMapping.from_dict(data)
assert mapping.tools["prefix_tool"].extension_name == "my-ext"
assert mapping.tools["my_tool"].extension_name == "my-ext"
assert mapping.hooks == {}

def test_from_dict_only_hooks(self):
Expand Down Expand Up @@ -902,7 +886,7 @@ def test_from_dict_with_source_new_format(self):
],
"source": {
"tools": {
"sap_mcp_servicenow_v1_create_ticket": {
"create_ticket": {
"extensionName": "servicenow-ext",
"extensionVersion": "2",
"extensionId": "uuid-sn",
Expand All @@ -919,18 +903,9 @@ def test_from_dict_with_source_new_format(self):
}
impl = ExtensionCapabilityImplementation.from_dict(data)
assert impl.source is not None
assert (
impl.source.tools["sap_mcp_servicenow_v1_create_ticket"].extension_name
== "servicenow-ext"
)
assert (
impl.source.tools["sap_mcp_servicenow_v1_create_ticket"].extension_version
== "2"
)
assert (
impl.source.tools["sap_mcp_servicenow_v1_create_ticket"].extension_id
== "uuid-sn"
)
assert impl.source.tools["create_ticket"].extension_name == "servicenow-ext"
assert impl.source.tools["create_ticket"].extension_version == "2"
assert impl.source.tools["create_ticket"].extension_id == "uuid-sn"
assert (
impl.source.hooks["3f5c8c8a-7b4d-4f9c-a4c0-7d5cb1a39f7e"].extension_name
== "workflow-ext"
Expand All @@ -942,20 +917,14 @@ def test_from_dict_with_source_old_format(self):
"capabilityId": "default",
"mcpServers": [],
"source": {
"tools": {"sap_mcp_servicenow_v1_create_ticket": "servicenow-ext"},
"tools": {"create_ticket": "servicenow-ext"},
"hooks": {"3f5c8c8a-7b4d-4f9c-a4c0-7d5cb1a39f7e": "workflow-ext"},
},
}
impl = ExtensionCapabilityImplementation.from_dict(data)
assert impl.source is not None
assert (
impl.source.tools["sap_mcp_servicenow_v1_create_ticket"].extension_name
== "servicenow-ext"
)
assert (
impl.source.tools["sap_mcp_servicenow_v1_create_ticket"].extension_version
== ""
)
assert impl.source.tools["create_ticket"].extension_name == "servicenow-ext"
assert impl.source.tools["create_ticket"].extension_version == ""
assert (
impl.source.hooks["3f5c8c8a-7b4d-4f9c-a4c0-7d5cb1a39f7e"].extension_name
== "workflow-ext"
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading