Skip to content

Commit 11ba7d1

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

8 files changed

Lines changed: 207 additions & 24 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: 67 additions & 4 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
@@ -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(identifier=slug, folder_path=folder_path)
187188
response = self.request(
188189
spec.method,
189190
url=spec.endpoint,
@@ -229,7 +230,67 @@ 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(identifier=slug, folder_path=folder_path)
234+
response = await self.request_async(
235+
spec.method,
236+
url=spec.endpoint,
237+
params=spec.params,
238+
headers=spec.headers,
239+
)
240+
return RemoteA2aAgent.model_validate(response.json())
241+
242+
@resource_override(
243+
resource_type="remoteA2aAgent",
244+
resource_identifier="name",
245+
legacy_resource_identifier="legacy_slug",
246+
)
247+
def retrieve_by_name(
248+
self,
249+
name: str,
250+
*,
251+
legacy_slug: str | None = None,
252+
folder_path: str | None = None,
253+
) -> RemoteA2aAgent:
254+
"""Retrieve a Remote A2A agent by its display name.
255+
256+
``legacy_slug`` supports already-deployed slug-keyed bindings during
257+
the name-identity transition.
258+
"""
259+
warnings.warn(
260+
"remote_a2a.retrieve_by_name is experimental and subject to change.",
261+
stacklevel=2,
262+
)
263+
spec = self._retrieve_spec(identifier=name, folder_path=folder_path)
264+
response = self.request(
265+
spec.method,
266+
url=spec.endpoint,
267+
params=spec.params,
268+
headers=spec.headers,
269+
)
270+
return RemoteA2aAgent.model_validate(response.json())
271+
272+
@resource_override(
273+
resource_type="remoteA2aAgent",
274+
resource_identifier="name",
275+
legacy_resource_identifier="legacy_slug",
276+
)
277+
async def retrieve_by_name_async(
278+
self,
279+
name: str,
280+
*,
281+
legacy_slug: str | None = None,
282+
folder_path: str | None = None,
283+
) -> RemoteA2aAgent:
284+
"""Retrieve a Remote A2A agent by its display name.
285+
286+
``legacy_slug`` supports already-deployed slug-keyed bindings during
287+
the name-identity transition.
288+
"""
289+
warnings.warn(
290+
"remote_a2a.retrieve_by_name_async is experimental and subject to change.",
291+
stacklevel=2,
292+
)
293+
spec = self._retrieve_spec(identifier=name, folder_path=folder_path)
233294
response = await self.request_async(
234295
spec.method,
235296
url=spec.endpoint,
@@ -279,14 +340,16 @@ def _list_spec(
279340

280341
def _retrieve_spec(
281342
self,
282-
slug: str,
343+
identifier: str,
283344
*,
284345
folder_path: str | None,
285346
) -> RequestSpec:
286347
folder_key = self._resolve_folder_key(folder_path)
287348
return RequestSpec(
288349
method="GET",
289-
endpoint=Endpoint(f"/agenthub_/api/remote-a2a-agents/{slug}"),
350+
endpoint=Endpoint(
351+
f"/agenthub_/api/remote-a2a-agents/{quote(identifier, safe='')}"
352+
),
290353
headers={
291354
**header_folder(folder_key, None),
292355
},

packages/uipath-platform/src/uipath/platform/common/_bindings.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ def resource_override(
219219
resource_type: str,
220220
resource_identifier: str = "name",
221221
folder_identifier: str = "folder_path",
222+
legacy_resource_identifier: str | None = None,
222223
) -> Callable[..., Any]:
223224
"""Decorator for applying resource overrides for an overridable resource.
224225
@@ -229,6 +230,9 @@ def resource_override(
229230
resource_type: Type of resource to check for overrides (e.g., "asset", "bucket")
230231
resource_identifier: Key name for the resource ID in override data (default: "name")
231232
folder_identifier: Key name for the folder path in override data (default: "folder_path")
233+
legacy_resource_identifier: Optional argument containing the identifier used
234+
by older binding keys. It is checked only when the primary identifier
235+
does not match.
232236
233237
Returns:
234238
Decorated function that receives overridden resource identifiers when applicable
@@ -257,19 +261,26 @@ def process_args(args, kwargs) -> dict[str, Any]:
257261
context_overwrites = _resource_overwrites.get()
258262

259263
if context_overwrites is not None:
260-
resource_identifier_value = all_args.get(resource_identifier)
261264
folder_identifier_value = all_args.get(folder_identifier)
262-
263-
key = f"{resource_type}.{resource_identifier_value}"
264-
# try to apply folder path, fallback to resource_type.resource_name
265-
if folder_identifier_value:
266-
key = (
267-
f"{key}.{folder_identifier_value}"
268-
if f"{key}.{folder_identifier_value}" in context_overwrites
269-
else key
270-
)
271-
272-
matched_overwrite = context_overwrites.get(key)
265+
identifier_values = [all_args.get(resource_identifier)]
266+
if legacy_resource_identifier is not None:
267+
identifier_values.append(all_args.get(legacy_resource_identifier))
268+
269+
matched_overwrite = None
270+
key = f"{resource_type}.{identifier_values[0]}"
271+
for identifier_value in identifier_values:
272+
if identifier_value is None:
273+
continue
274+
candidate_key = f"{resource_type}.{identifier_value}"
275+
# Try the folder-qualified key first, then the bare key.
276+
if folder_identifier_value:
277+
folder_key = f"{candidate_key}.{folder_identifier_value}"
278+
if folder_key in context_overwrites:
279+
candidate_key = folder_key
280+
matched_overwrite = context_overwrites.get(candidate_key)
281+
key = candidate_key
282+
if matched_overwrite is not None:
283+
break
273284

274285
# Apply the matched overwrite
275286
if matched_overwrite is not None:

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

Lines changed: 59 additions & 4 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

@@ -137,7 +138,7 @@ def retrieve(
137138
```
138139
"""
139140
spec = self._retrieve_spec(
140-
slug=slug,
141+
identifier=slug,
141142
folder_path=folder_path,
142143
)
143144

@@ -183,7 +184,7 @@ async def main():
183184
```
184185
"""
185186
spec = self._retrieve_spec(
186-
slug=slug,
187+
identifier=slug,
187188
folder_path=folder_path,
188189
)
189190

@@ -196,6 +197,60 @@ async def main():
196197

197198
return McpServer.model_validate(response.json())
198199

200+
@resource_override(
201+
resource_type="mcpServer",
202+
resource_identifier="name",
203+
legacy_resource_identifier="legacy_slug",
204+
)
205+
@traced(name="mcp_retrieve_by_name", run_type="uipath")
206+
def retrieve_by_name(
207+
self,
208+
name: str,
209+
*,
210+
legacy_slug: str | None = None,
211+
folder_path: str | None = None,
212+
) -> McpServer:
213+
"""Retrieve an MCP server by its display name.
214+
215+
``legacy_slug`` lets already-deployed slug-keyed bindings apply their
216+
overwrite while packages transition to name-keyed bindings.
217+
"""
218+
spec = self._retrieve_spec(identifier=name, folder_path=folder_path)
219+
response = self.request(
220+
spec.method,
221+
url=spec.endpoint,
222+
params=spec.params,
223+
headers=spec.headers,
224+
)
225+
return McpServer.model_validate(response.json())
226+
227+
@resource_override(
228+
resource_type="mcpServer",
229+
resource_identifier="name",
230+
legacy_resource_identifier="legacy_slug",
231+
)
232+
@traced(name="mcp_retrieve_by_name", run_type="uipath")
233+
async def retrieve_by_name_async(
234+
self,
235+
name: str,
236+
*,
237+
legacy_slug: str | None = None,
238+
folder_path: str | None = None,
239+
) -> McpServer:
240+
"""Retrieve an MCP server by its display name.
241+
242+
``legacy_slug`` lets already-deployed slug-keyed bindings apply their
243+
overwrite while packages transition to name-keyed bindings.
244+
"""
245+
spec = self._retrieve_spec(identifier=name, folder_path=folder_path)
246+
response = await self.request_async(
247+
spec.method,
248+
url=spec.endpoint,
249+
params=spec.params,
250+
headers=spec.headers,
251+
)
252+
return McpServer.model_validate(response.json())
253+
199254
@property
200255
def custom_headers(self) -> dict[str, str]:
201256
return self.folder_headers
@@ -223,14 +278,14 @@ def _list_spec(
223278

224279
def _retrieve_spec(
225280
self,
226-
slug: str,
281+
identifier: str,
227282
*,
228283
folder_path: str | None,
229284
) -> RequestSpec:
230285
folder_key = self._resolve_folder_key(folder_path)
231286
return RequestSpec(
232287
method="GET",
233-
endpoint=Endpoint(f"/agenthub_/api/servers/{slug}"),
288+
endpoint=Endpoint(f"/agenthub_/api/servers/{quote(identifier, safe='')}"),
234289
headers={
235290
**header_folder(folder_key, None),
236291
},

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,3 +569,9 @@ def test_validate_swagger_server(self) -> None:
569569
)
570570
assert server.type == 7
571571
assert server.slug == "contoso-directory"
572+
573+
574+
def test_mcp_retrieve_spec_encodes_display_name(service: McpService) -> None:
575+
spec = service._retrieve_spec(identifier="Friendly MCP/Europe", folder_path=None)
576+
577+
assert "api/servers/Friendly%20MCP%2FEurope" in str(spec.endpoint)

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def test_falls_back_to_folder_context_when_folder_path_missing(
3636
self, service: RemoteA2aService
3737
) -> None:
3838
"""No folder_path (e.g. local debug) must not raise; it falls back to context."""
39-
spec = service._retrieve_spec(slug="weather", folder_path=None)
39+
spec = service._retrieve_spec(identifier="weather", folder_path=None)
4040

4141
assert "remote-a2a-agents/weather" in str(spec.endpoint)
4242
assert spec.headers[HEADER_FOLDER_KEY] == "context-folder-key"
@@ -50,6 +50,15 @@ def test_resolves_explicit_folder_path(
5050
lambda folder_path: "resolved-folder-key",
5151
)
5252

53-
spec = service._retrieve_spec(slug="weather", folder_path="MyFolder")
53+
spec = service._retrieve_spec(identifier="weather", folder_path="MyFolder")
5454

5555
assert spec.headers[HEADER_FOLDER_KEY] == "resolved-folder-key"
56+
57+
def test_encodes_display_name_in_lookup_path(
58+
self, service: RemoteA2aService
59+
) -> None:
60+
spec = service._retrieve_spec(
61+
identifier="Friendly Agent/Europe", folder_path=None
62+
)
63+
64+
assert "remote-a2a-agents/Friendly%20Agent%2FEurope" in str(spec.endpoint)

packages/uipath-platform/uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/uipath/tests/resource_overrides/test_resource_overrides.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,45 @@ def some_method(a: str, b: str, **kwargs):
328328
assert len(result) == 1
329329
assert result["kwargs_param"] == 123
330330

331+
@pytest.mark.anyio
332+
async def test_overrides_decorator_supports_legacy_binding_key(self):
333+
from uipath.platform.common import (
334+
GenericResourceOverwrite,
335+
ResourceOverwritesContext,
336+
resource_override,
337+
)
338+
339+
@resource_override(
340+
resource_type="mcpServer",
341+
resource_identifier="name",
342+
legacy_resource_identifier="legacy_slug",
343+
)
344+
async def retrieve_by_name(
345+
name: str,
346+
*,
347+
legacy_slug: str,
348+
folder_path: str,
349+
):
350+
return name, folder_path
351+
352+
async def get_overwrites():
353+
return {
354+
"mcpServer.legacy-slug": GenericResourceOverwrite(
355+
resource_type="mcpServer",
356+
name="Replacement MCP",
357+
folder_path="Replacement/Folder",
358+
)
359+
}
360+
361+
async with ResourceOverwritesContext(get_overwrites):
362+
result = await retrieve_by_name(
363+
"Friendly MCP",
364+
legacy_slug="legacy-slug",
365+
folder_path="Original/Folder",
366+
)
367+
368+
assert result == ("Replacement MCP", "Replacement/Folder")
369+
331370
def test_debug_with_resource_overwrites(
332371
self,
333372
runner: CliRunner,

0 commit comments

Comments
 (0)