Skip to content

Commit be31ae4

Browse files
committed
fix: add name-based MCP and A2A lookup
1 parent bb00412 commit be31ae4

8 files changed

Lines changed: 205 additions & 34 deletions

File tree

packages/uipath-platform/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-platform"
3-
version = "0.2.13"
3+
version = "0.2.14"
44
description = "HTTP client library for programmatic access to UiPath Platform"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

packages/uipath-platform/src/uipath/platform/agenthub/_remote_a2a_service.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import warnings
99
from typing import Any, List
10+
from urllib.parse import quote
1011

1112
from ..common._base_service import BaseService
1213
from ..common._bindings import resource_override
@@ -150,20 +151,20 @@ async def main():
150151
data = response.json()
151152
return [RemoteA2aAgent.model_validate(agent) for agent in data.get("value", [])]
152153

153-
@resource_override(resource_type="remoteA2aAgent", resource_identifier="slug")
154+
@resource_override(resource_type="remoteA2aAgent", resource_identifier="name")
154155
def retrieve(
155156
self,
156-
slug: str,
157+
name: str,
157158
*,
158159
folder_path: str | None = None,
159160
) -> RemoteA2aAgent:
160-
"""Retrieve a specific Remote A2A agent by slug.
161+
"""Retrieve a specific Remote A2A agent by display name.
161162
162163
.. warning::
163164
This method is experimental and subject to change.
164165
165166
Args:
166-
slug: The unique slug identifier for the agent.
167+
name: The display name of the agent.
167168
folder_path: The folder path where the agent is located.
168169
169170
Returns:
@@ -183,7 +184,7 @@ def retrieve(
183184
"remote_a2a.retrieve is experimental and subject to change.",
184185
stacklevel=2,
185186
)
186-
spec = self._retrieve_spec(slug=slug, folder_path=folder_path)
187+
spec = self._retrieve_spec(name=name, folder_path=folder_path)
187188
response = self.request(
188189
spec.method,
189190
url=spec.endpoint,
@@ -192,20 +193,20 @@ def retrieve(
192193
)
193194
return RemoteA2aAgent.model_validate(response.json())
194195

195-
@resource_override(resource_type="remoteA2aAgent", resource_identifier="slug")
196+
@resource_override(resource_type="remoteA2aAgent", resource_identifier="name")
196197
async def retrieve_async(
197198
self,
198-
slug: str,
199+
name: str,
199200
*,
200201
folder_path: str | None = None,
201202
) -> RemoteA2aAgent:
202-
"""Asynchronously retrieve a specific Remote A2A agent by slug.
203+
"""Asynchronously retrieve a specific Remote A2A agent by display name.
203204
204205
.. warning::
205206
This method is experimental and subject to change.
206207
207208
Args:
208-
slug: The unique slug identifier for the agent.
209+
name: The display name of the agent.
209210
folder_path: The folder path where the agent is located.
210211
211212
Returns:
@@ -229,7 +230,7 @@ async def main():
229230
"remote_a2a.retrieve_async is experimental and subject to change.",
230231
stacklevel=2,
231232
)
232-
spec = self._retrieve_spec(slug=slug, folder_path=folder_path)
233+
spec = self._retrieve_spec(name=name, folder_path=folder_path)
233234
response = await self.request_async(
234235
spec.method,
235236
url=spec.endpoint,
@@ -279,14 +280,16 @@ def _list_spec(
279280

280281
def _retrieve_spec(
281282
self,
282-
slug: str,
283+
name: str,
283284
*,
284285
folder_path: str | None,
285286
) -> RequestSpec:
286287
folder_key = self._resolve_folder_key(folder_path)
287288
return RequestSpec(
288289
method="GET",
289-
endpoint=Endpoint(f"/agenthub_/api/remote-a2a-agents/{slug}"),
290+
endpoint=Endpoint(
291+
f"/agenthub_/api/remote-a2a-agents/{quote(name, safe='')}"
292+
),
290293
headers={
291294
**header_folder(folder_key, None),
292295
},

packages/uipath-platform/src/uipath/platform/orchestrator/_mcp_service.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from typing import List
2+
from urllib.parse import quote
23

34
from uipath.core.tracing import traced
45

@@ -109,18 +110,18 @@ async def main():
109110

110111
return [McpServer.model_validate(server) for server in response.json()]
111112

112-
@resource_override(resource_type="mcpServer", resource_identifier="slug")
113+
@resource_override(resource_type="mcpServer", resource_identifier="name")
113114
@traced(name="mcp_retrieve", run_type="uipath")
114115
def retrieve(
115116
self,
116-
slug: str,
117+
name: str,
117118
*,
118119
folder_path: str | None = None,
119120
) -> McpServer:
120-
"""Retrieve a specific MCP server by its slug.
121+
"""Retrieve a specific MCP server by its display name.
121122
122123
Args:
123-
slug (str): The unique slug identifier for the server.
124+
name (str): The display name of the server.
124125
folder_path (Optional[str]): The path of the folder where the server is located.
125126
126127
Returns:
@@ -132,12 +133,12 @@ def retrieve(
132133
133134
client = UiPath()
134135
135-
server = client.mcp.retrieve(slug="my-server-slug", folder_path="MyFolder")
136+
server = client.mcp.retrieve(name="My Server", folder_path="MyFolder")
136137
print(f"Server: {server.name}, URL: {server.mcp_url}")
137138
```
138139
"""
139140
spec = self._retrieve_spec(
140-
slug=slug,
141+
name=name,
141142
folder_path=folder_path,
142143
)
143144

@@ -150,18 +151,18 @@ def retrieve(
150151

151152
return McpServer.model_validate(response.json())
152153

153-
@resource_override(resource_type="mcpServer", resource_identifier="slug")
154+
@resource_override(resource_type="mcpServer", resource_identifier="name")
154155
@traced(name="mcp_retrieve", run_type="uipath")
155156
async def retrieve_async(
156157
self,
157-
slug: str,
158+
name: str,
158159
*,
159160
folder_path: str | None = None,
160161
) -> McpServer:
161-
"""Asynchronously retrieve a specific MCP server by its slug.
162+
"""Asynchronously retrieve a specific MCP server by its display name.
162163
163164
Args:
164-
slug (str): The unique slug identifier for the server.
165+
name (str): The display name of the server.
165166
folder_path (Optional[str]): The path of the folder where the server is located.
166167
167168
Returns:
@@ -176,14 +177,14 @@ async def retrieve_async(
176177
sdk = UiPath()
177178
178179
async def main():
179-
server = await sdk.mcp.retrieve_async(slug="my-server-slug", folder_path="MyFolder")
180+
server = await sdk.mcp.retrieve_async(name="My Server", folder_path="MyFolder")
180181
print(f"Server: {server.name}, URL: {server.mcp_url}")
181182
182183
asyncio.run(main())
183184
```
184185
"""
185186
spec = self._retrieve_spec(
186-
slug=slug,
187+
name=name,
187188
folder_path=folder_path,
188189
)
189190

@@ -223,14 +224,14 @@ def _list_spec(
223224

224225
def _retrieve_spec(
225226
self,
226-
slug: str,
227+
name: str,
227228
*,
228229
folder_path: str | None,
229230
) -> RequestSpec:
230231
folder_key = self._resolve_folder_key(folder_path)
231232
return RequestSpec(
232233
method="GET",
233-
endpoint=Endpoint(f"/agenthub_/api/servers/{slug}"),
234+
endpoint=Endpoint(f"/agenthub_/api/servers/{quote(name, safe='')}"),
234235
headers={
235236
**header_folder(folder_key, None),
236237
},

packages/uipath-platform/tests/services/test_mcp_service.py

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
from unittest.mock import Mock, patch
1+
from unittest.mock import AsyncMock, Mock, patch
22

33
import pytest
44
from pytest_httpx import HTTPXMock
55

66
from uipath.platform import UiPathApiConfig, UiPathExecutionContext
7+
from uipath.platform.common._bindings import (
8+
GenericResourceOverwrite,
9+
_resource_overwrites,
10+
)
711
from uipath.platform.constants import HEADER_FOLDER_KEY, HEADER_USER_AGENT
812
from uipath.platform.orchestrator import McpService
913
from uipath.platform.orchestrator._folder_service import FolderService
@@ -363,6 +367,77 @@ async def test_retrieve_server_async(
363367
== f"UiPath.Python.Sdk/UiPath.Python.Sdk.Activities.McpService.retrieve_async/{version}"
364368
)
365369

370+
def test_retrieve_server_by_name(self, service: McpService) -> None:
371+
response = Mock()
372+
response.json.return_value = {
373+
"name": "Friendly MCP/Europe",
374+
"slug": "friendly-mcp-europe",
375+
}
376+
377+
with patch.object(service, "request", return_value=response) as request:
378+
server = service.retrieve("Friendly MCP/Europe")
379+
380+
assert server.name == "Friendly MCP/Europe"
381+
assert "api/servers/Friendly%20MCP%2FEurope" in str(
382+
request.call_args.kwargs["url"]
383+
)
384+
385+
def test_retrieve_applies_display_name_binding(
386+
self, service: McpService
387+
) -> None:
388+
response = Mock()
389+
response.json.return_value = {
390+
"name": "Replacement MCP",
391+
"slug": "replacement-mcp",
392+
}
393+
overwrite = GenericResourceOverwrite(
394+
resource_type="mcpServer",
395+
name="Replacement MCP",
396+
folder_path="Replacement Folder",
397+
)
398+
token = _resource_overwrites.set({"mcpServer.Original MCP": overwrite})
399+
400+
try:
401+
with (
402+
patch.object(service, "request", return_value=response) as request,
403+
patch.object(
404+
service._folders_service,
405+
"retrieve_folder_key",
406+
return_value="replacement-folder-key",
407+
),
408+
):
409+
service.retrieve("Original MCP")
410+
finally:
411+
_resource_overwrites.reset(token)
412+
413+
assert "api/servers/Replacement%20MCP" in str(
414+
request.call_args.kwargs["url"]
415+
)
416+
assert (
417+
request.call_args.kwargs["headers"][HEADER_FOLDER_KEY]
418+
== "replacement-folder-key"
419+
)
420+
421+
@pytest.mark.anyio
422+
async def test_retrieve_server_by_name_async(self, service: McpService) -> None:
423+
response = Mock()
424+
response.json.return_value = {
425+
"name": "Friendly MCP/Europe",
426+
"slug": "friendly-mcp-europe",
427+
}
428+
429+
with patch.object(
430+
service,
431+
"request_async",
432+
new=AsyncMock(return_value=response),
433+
) as request:
434+
server = await service.retrieve_async("Friendly MCP/Europe")
435+
436+
assert server.name == "Friendly MCP/Europe"
437+
assert "api/servers/Friendly%20MCP%2FEurope" in str(
438+
request.call_args.kwargs["url"]
439+
)
440+
366441
class TestRequestKwargs:
367442
"""Test that all methods pass the correct kwargs to request/request_async."""
368443

@@ -569,3 +644,9 @@ def test_validate_swagger_server(self) -> None:
569644
)
570645
assert server.type == 7
571646
assert server.slug == "contoso-directory"
647+
648+
649+
def test_mcp_retrieve_spec_encodes_display_name(service: McpService) -> None:
650+
spec = service._retrieve_spec(name="Friendly MCP/Europe", folder_path=None)
651+
652+
assert "api/servers/Friendly%20MCP%2FEurope" in str(spec.endpoint)

0 commit comments

Comments
 (0)