Skip to content

Commit 6d21593

Browse files
radugheoclaude
andcommitted
feat: introduce ToolsConfiguration on AgentMcpResourceConfig
Adds Cached / Dynamic discriminated union under a new ToolsConfiguration model accessed via tools_configuration.cached_behaviour on the MCP resource. Cached corresponds to the old dynamic_tools=none; Dynamic corresponds to dynamic_tools=all. Schema mode is dropped from the new model for now and will be reintroduced in a later sprint. The old dynamic_tools field is removed from the model. Existing agent.json files that still serialize dynamicTools (any value) continue to parse unchanged because BaseCfg keeps extra="allow"; the legacy DynamicToolsMode enum stays exported as a deprecated alias. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 010a8b7 commit 6d21593

4 files changed

Lines changed: 156 additions & 6 deletions

File tree

packages/uipath/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"
3-
version = "2.10.67"
3+
version = "2.10.68"
44
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

packages/uipath/src/uipath/agent/models/agent.py

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -446,13 +446,55 @@ class AgentMcpTool(BaseCfg):
446446

447447

448448
class DynamicToolsMode(str, CaseInsensitiveEnum):
449-
"""Dynamic tools mode enumeration."""
449+
"""Dynamic tools mode enumeration.
450+
451+
Deprecated: kept for backwards compatibility with older ``agent.json`` files
452+
that still serialize the ``dynamicTools`` field. New code should use
453+
:class:`ToolsConfiguration` (see ``AgentMcpResourceConfig.tools_configuration``).
454+
"""
450455

451456
NONE = "none"
452457
SCHEMA = "schema"
453458
ALL = "all"
454459

455460

461+
class Cached(BaseCfg):
462+
"""Cached tools configuration: use the tools saved in the agent definition snapshot."""
463+
464+
type: Literal["cached"] = Field(default="cached", frozen=True)
465+
466+
467+
class Schema(BaseCfg):
468+
"""Schema tools configuration: server discovery filtered by snapshot allowlist.
469+
470+
Fetches the live tool list from the MCP server, but keeps only the tools
471+
whose names are in the snapshot's ``available_tools``.
472+
473+
Not currently exposed in the frontend; the runtime continues to honor it so
474+
the option can be re-enabled later without further model changes.
475+
"""
476+
477+
type: Literal["schema"] = Field(default="schema", frozen=True)
478+
479+
480+
class Dynamic(BaseCfg):
481+
"""Dynamic tools configuration: use whatever tools the MCP server exposes at runtime, with live schemas."""
482+
483+
type: Literal["dynamic"] = Field(default="dynamic", frozen=True)
484+
485+
486+
CachedBehaviour = Annotated[
487+
Union[Cached, Schema, Dynamic],
488+
Field(discriminator="type"),
489+
]
490+
491+
492+
class ToolsConfiguration(BaseCfg):
493+
"""Configuration describing how tools are sourced for an MCP resource."""
494+
495+
cached_behaviour: CachedBehaviour = Field(alias="cachedBehaviour")
496+
497+
456498
class AgentMcpResourceConfig(BaseAgentResourceConfig):
457499
"""Agent MCP resource configuration model."""
458500

@@ -462,8 +504,8 @@ class AgentMcpResourceConfig(BaseAgentResourceConfig):
462504
folder_path: str = Field(alias="folderPath")
463505
slug: str = Field(..., alias="slug")
464506
available_tools: List[AgentMcpTool] = Field(..., alias="availableTools")
465-
dynamic_tools: DynamicToolsMode = Field(
466-
default=DynamicToolsMode.NONE, alias="dynamicTools"
507+
tools_configuration: Optional[ToolsConfiguration] = Field(
508+
default=None, alias="toolsConfiguration"
467509
)
468510

469511

@@ -1337,6 +1379,27 @@ def _normalize_resources(v: Dict[str, Any]) -> None:
13371379
)
13381380
res["settings"] = settings
13391381

1382+
if res["$resourceType"] == "mcp":
1383+
# Legacy migration: translate `dynamicTools` (string enum) into
1384+
# `toolsConfiguration.cachedBehaviour` (discriminated union) when
1385+
# the new field is absent. Without this, old agent.json files
1386+
# with `dynamicTools="all"` would silently fall back to Cached.
1387+
has_new_field = (
1388+
"toolsConfiguration" in res or "tools_configuration" in res
1389+
)
1390+
if not has_new_field:
1391+
legacy = res.get("dynamicTools") or res.get("dynamic_tools")
1392+
if isinstance(legacy, str):
1393+
mapped_type = {
1394+
"none": "cached",
1395+
"schema": "schema",
1396+
"all": "dynamic",
1397+
}.get(legacy.lower())
1398+
if mapped_type is not None:
1399+
res["toolsConfiguration"] = {
1400+
"cachedBehaviour": {"type": mapped_type}
1401+
}
1402+
13401403
out.append(res)
13411404

13421405
v["resources"] = out

packages/uipath/tests/agent/models/test_agent.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3950,3 +3950,90 @@ def test_argument_group_name_recipient_missing_argument_name_raises(self):
39503950
payload = {"type": 8}
39513951
with pytest.raises(ValidationError):
39523952
TypeAdapter(AgentEscalationRecipient).validate_python(payload)
3953+
3954+
3955+
class TestMcpToolsConfigurationLegacyMigration:
3956+
"""Legacy `dynamicTools` is migrated into `toolsConfiguration` when missing."""
3957+
3958+
@staticmethod
3959+
def _mcp_resource(extra: dict[str, Any]) -> dict[str, Any]:
3960+
return {
3961+
"$resourceType": "mcp",
3962+
"id": "11111111-1111-1111-1111-111111111111",
3963+
"name": "mcp_server",
3964+
"description": "Test MCP",
3965+
"isEnabled": True,
3966+
"slug": "mcp-server-time",
3967+
"folderPath": "/Shared",
3968+
"availableTools": [],
3969+
**extra,
3970+
}
3971+
3972+
@staticmethod
3973+
def _agent_def_payload(mcp_resource: dict[str, Any]) -> dict[str, Any]:
3974+
return {
3975+
"id": "00000000-0000-0000-0000-000000000000",
3976+
"version": "1.1.0",
3977+
"name": "Agent",
3978+
"inputSchema": {"type": "object", "properties": {}},
3979+
"outputSchema": {"type": "object", "properties": {}},
3980+
"settings": {
3981+
"model": "gpt-5.4",
3982+
"maxTokens": 1000,
3983+
"temperature": 0,
3984+
"engine": "basic-v2",
3985+
"maxIterations": 10,
3986+
"mode": "standard",
3987+
},
3988+
"messages": [],
3989+
"resources": [mcp_resource],
3990+
"guardrails": [],
3991+
}
3992+
3993+
@pytest.mark.parametrize(
3994+
"legacy, expected_type",
3995+
[
3996+
("none", "cached"),
3997+
("schema", "schema"),
3998+
("all", "dynamic"),
3999+
("ALL", "dynamic"), # case-insensitive
4000+
],
4001+
)
4002+
def test_legacy_dynamic_tools_migrates_to_tools_configuration(
4003+
self, legacy: str, expected_type: str
4004+
):
4005+
payload = self._agent_def_payload(self._mcp_resource({"dynamicTools": legacy}))
4006+
config: AgentDefinition = TypeAdapter(AgentDefinition).validate_python(payload)
4007+
mcp = next(
4008+
r for r in config.resources if r.resource_type == AgentResourceType.MCP
4009+
)
4010+
assert isinstance(mcp, AgentMcpResourceConfig)
4011+
assert mcp.tools_configuration is not None
4012+
assert mcp.tools_configuration.cached_behaviour.type == expected_type
4013+
4014+
def test_new_field_wins_when_both_are_present(self):
4015+
# toolsConfiguration is the source of truth when both fields are set.
4016+
payload = self._agent_def_payload(
4017+
self._mcp_resource(
4018+
{
4019+
"dynamicTools": "all",
4020+
"toolsConfiguration": {"cachedBehaviour": {"type": "cached"}},
4021+
}
4022+
)
4023+
)
4024+
config: AgentDefinition = TypeAdapter(AgentDefinition).validate_python(payload)
4025+
mcp = next(
4026+
r for r in config.resources if r.resource_type == AgentResourceType.MCP
4027+
)
4028+
assert isinstance(mcp, AgentMcpResourceConfig)
4029+
assert mcp.tools_configuration is not None
4030+
assert mcp.tools_configuration.cached_behaviour.type == "cached"
4031+
4032+
def test_missing_both_fields_leaves_configuration_unset(self):
4033+
payload = self._agent_def_payload(self._mcp_resource({}))
4034+
config: AgentDefinition = TypeAdapter(AgentDefinition).validate_python(payload)
4035+
mcp = next(
4036+
r for r in config.resources if r.resource_type == AgentResourceType.MCP
4037+
)
4038+
assert isinstance(mcp, AgentMcpResourceConfig)
4039+
assert mcp.tools_configuration is None

packages/uipath/uv.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)