hud-python 0.4.45__py3-none-any.whl → 0.5.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. hud/__init__.py +27 -7
  2. hud/agents/__init__.py +70 -5
  3. hud/agents/base.py +238 -500
  4. hud/agents/claude.py +236 -247
  5. hud/agents/gateway.py +42 -0
  6. hud/agents/gemini.py +264 -0
  7. hud/agents/gemini_cua.py +324 -0
  8. hud/agents/grounded_openai.py +98 -100
  9. hud/agents/misc/integration_test_agent.py +51 -20
  10. hud/agents/misc/response_agent.py +48 -36
  11. hud/agents/openai.py +282 -296
  12. hud/agents/{openai_chat_generic.py → openai_chat.py} +63 -33
  13. hud/agents/operator.py +199 -0
  14. hud/agents/resolver.py +70 -0
  15. hud/agents/tests/conftest.py +133 -0
  16. hud/agents/tests/test_base.py +300 -622
  17. hud/agents/tests/test_base_runtime.py +233 -0
  18. hud/agents/tests/test_claude.py +381 -214
  19. hud/agents/tests/test_client.py +9 -10
  20. hud/agents/tests/test_gemini.py +369 -0
  21. hud/agents/tests/test_grounded_openai_agent.py +65 -50
  22. hud/agents/tests/test_openai.py +377 -140
  23. hud/agents/tests/test_operator.py +362 -0
  24. hud/agents/tests/test_resolver.py +192 -0
  25. hud/agents/tests/test_run_eval.py +179 -0
  26. hud/agents/types.py +148 -0
  27. hud/cli/__init__.py +493 -546
  28. hud/cli/analyze.py +43 -5
  29. hud/cli/build.py +699 -113
  30. hud/cli/debug.py +8 -5
  31. hud/cli/dev.py +889 -732
  32. hud/cli/eval.py +793 -667
  33. hud/cli/flows/dev.py +167 -0
  34. hud/cli/flows/init.py +191 -0
  35. hud/cli/flows/tasks.py +153 -56
  36. hud/cli/flows/templates.py +151 -0
  37. hud/cli/flows/tests/__init__.py +1 -0
  38. hud/cli/flows/tests/test_dev.py +126 -0
  39. hud/cli/init.py +60 -58
  40. hud/cli/pull.py +1 -1
  41. hud/cli/push.py +38 -13
  42. hud/cli/rft.py +311 -0
  43. hud/cli/rft_status.py +145 -0
  44. hud/cli/tests/test_analyze.py +5 -5
  45. hud/cli/tests/test_analyze_metadata.py +3 -2
  46. hud/cli/tests/test_analyze_module.py +120 -0
  47. hud/cli/tests/test_build.py +110 -8
  48. hud/cli/tests/test_build_failure.py +41 -0
  49. hud/cli/tests/test_build_module.py +50 -0
  50. hud/cli/tests/test_cli_init.py +6 -1
  51. hud/cli/tests/test_cli_more_wrappers.py +30 -0
  52. hud/cli/tests/test_cli_root.py +140 -0
  53. hud/cli/tests/test_convert.py +361 -0
  54. hud/cli/tests/test_debug.py +12 -10
  55. hud/cli/tests/test_dev.py +197 -0
  56. hud/cli/tests/test_eval.py +251 -0
  57. hud/cli/tests/test_eval_bedrock.py +51 -0
  58. hud/cli/tests/test_init.py +124 -0
  59. hud/cli/tests/test_main_module.py +11 -5
  60. hud/cli/tests/test_mcp_server.py +12 -100
  61. hud/cli/tests/test_push.py +1 -1
  62. hud/cli/tests/test_push_happy.py +74 -0
  63. hud/cli/tests/test_push_wrapper.py +23 -0
  64. hud/cli/tests/test_registry.py +1 -1
  65. hud/cli/tests/test_utils.py +1 -1
  66. hud/cli/{rl → utils}/celebrate.py +14 -12
  67. hud/cli/utils/config.py +18 -1
  68. hud/cli/utils/docker.py +130 -4
  69. hud/cli/utils/env_check.py +9 -9
  70. hud/cli/utils/git.py +136 -0
  71. hud/cli/utils/interactive.py +39 -5
  72. hud/cli/utils/metadata.py +70 -1
  73. hud/cli/utils/runner.py +1 -1
  74. hud/cli/utils/server.py +2 -2
  75. hud/cli/utils/source_hash.py +3 -3
  76. hud/cli/utils/tasks.py +4 -1
  77. hud/cli/utils/tests/__init__.py +0 -0
  78. hud/cli/utils/tests/test_config.py +58 -0
  79. hud/cli/utils/tests/test_docker.py +93 -0
  80. hud/cli/utils/tests/test_docker_hints.py +71 -0
  81. hud/cli/utils/tests/test_env_check.py +74 -0
  82. hud/cli/utils/tests/test_environment.py +42 -0
  83. hud/cli/utils/tests/test_git.py +142 -0
  84. hud/cli/utils/tests/test_interactive_module.py +60 -0
  85. hud/cli/utils/tests/test_local_runner.py +50 -0
  86. hud/cli/utils/tests/test_logging_utils.py +23 -0
  87. hud/cli/utils/tests/test_metadata.py +49 -0
  88. hud/cli/utils/tests/test_package_runner.py +35 -0
  89. hud/cli/utils/tests/test_registry_utils.py +49 -0
  90. hud/cli/utils/tests/test_remote_runner.py +25 -0
  91. hud/cli/utils/tests/test_runner_modules.py +52 -0
  92. hud/cli/utils/tests/test_source_hash.py +36 -0
  93. hud/cli/utils/tests/test_tasks.py +80 -0
  94. hud/cli/utils/version_check.py +258 -0
  95. hud/cli/{rl → utils}/viewer.py +2 -2
  96. hud/clients/README.md +12 -11
  97. hud/clients/__init__.py +4 -3
  98. hud/clients/base.py +166 -26
  99. hud/clients/environment.py +51 -0
  100. hud/clients/fastmcp.py +13 -6
  101. hud/clients/mcp_use.py +45 -15
  102. hud/clients/tests/test_analyze_scenarios.py +206 -0
  103. hud/clients/tests/test_protocol.py +9 -3
  104. hud/datasets/__init__.py +23 -20
  105. hud/datasets/loader.py +326 -0
  106. hud/datasets/runner.py +198 -105
  107. hud/datasets/tests/__init__.py +0 -0
  108. hud/datasets/tests/test_loader.py +221 -0
  109. hud/datasets/tests/test_utils.py +315 -0
  110. hud/datasets/utils.py +270 -90
  111. hud/environment/__init__.py +52 -0
  112. hud/environment/connection.py +258 -0
  113. hud/environment/connectors/__init__.py +33 -0
  114. hud/environment/connectors/base.py +68 -0
  115. hud/environment/connectors/local.py +177 -0
  116. hud/environment/connectors/mcp_config.py +137 -0
  117. hud/environment/connectors/openai.py +101 -0
  118. hud/environment/connectors/remote.py +172 -0
  119. hud/environment/environment.py +835 -0
  120. hud/environment/integrations/__init__.py +45 -0
  121. hud/environment/integrations/adk.py +67 -0
  122. hud/environment/integrations/anthropic.py +196 -0
  123. hud/environment/integrations/gemini.py +92 -0
  124. hud/environment/integrations/langchain.py +82 -0
  125. hud/environment/integrations/llamaindex.py +68 -0
  126. hud/environment/integrations/openai.py +238 -0
  127. hud/environment/mock.py +306 -0
  128. hud/environment/router.py +263 -0
  129. hud/environment/scenarios.py +620 -0
  130. hud/environment/tests/__init__.py +1 -0
  131. hud/environment/tests/test_connection.py +317 -0
  132. hud/environment/tests/test_connectors.py +205 -0
  133. hud/environment/tests/test_environment.py +593 -0
  134. hud/environment/tests/test_integrations.py +257 -0
  135. hud/environment/tests/test_local_connectors.py +242 -0
  136. hud/environment/tests/test_scenarios.py +1086 -0
  137. hud/environment/tests/test_tools.py +208 -0
  138. hud/environment/types.py +23 -0
  139. hud/environment/utils/__init__.py +35 -0
  140. hud/environment/utils/formats.py +215 -0
  141. hud/environment/utils/schema.py +171 -0
  142. hud/environment/utils/tool_wrappers.py +113 -0
  143. hud/eval/__init__.py +67 -0
  144. hud/eval/context.py +727 -0
  145. hud/eval/display.py +299 -0
  146. hud/eval/instrument.py +187 -0
  147. hud/eval/manager.py +533 -0
  148. hud/eval/parallel.py +268 -0
  149. hud/eval/task.py +372 -0
  150. hud/eval/tests/__init__.py +1 -0
  151. hud/eval/tests/test_context.py +178 -0
  152. hud/eval/tests/test_eval.py +210 -0
  153. hud/eval/tests/test_manager.py +152 -0
  154. hud/eval/tests/test_parallel.py +168 -0
  155. hud/eval/tests/test_task.py +291 -0
  156. hud/eval/types.py +65 -0
  157. hud/eval/utils.py +194 -0
  158. hud/patches/__init__.py +19 -0
  159. hud/patches/mcp_patches.py +308 -0
  160. hud/patches/warnings.py +54 -0
  161. hud/samples/browser.py +4 -4
  162. hud/server/__init__.py +2 -1
  163. hud/server/low_level.py +2 -1
  164. hud/server/router.py +164 -0
  165. hud/server/server.py +567 -80
  166. hud/server/tests/test_mcp_server_integration.py +11 -11
  167. hud/server/tests/test_mcp_server_more.py +1 -1
  168. hud/server/tests/test_server_extra.py +2 -0
  169. hud/settings.py +45 -3
  170. hud/shared/exceptions.py +36 -10
  171. hud/shared/hints.py +26 -1
  172. hud/shared/requests.py +15 -3
  173. hud/shared/tests/test_exceptions.py +40 -31
  174. hud/shared/tests/test_hints.py +167 -0
  175. hud/telemetry/__init__.py +20 -19
  176. hud/telemetry/exporter.py +201 -0
  177. hud/telemetry/instrument.py +165 -253
  178. hud/telemetry/tests/test_eval_telemetry.py +356 -0
  179. hud/telemetry/tests/test_exporter.py +258 -0
  180. hud/telemetry/tests/test_instrument.py +401 -0
  181. hud/tools/__init__.py +18 -2
  182. hud/tools/agent.py +223 -0
  183. hud/tools/apply_patch.py +639 -0
  184. hud/tools/base.py +54 -4
  185. hud/tools/bash.py +2 -2
  186. hud/tools/computer/__init__.py +36 -3
  187. hud/tools/computer/anthropic.py +2 -2
  188. hud/tools/computer/gemini.py +385 -0
  189. hud/tools/computer/hud.py +23 -6
  190. hud/tools/computer/openai.py +20 -21
  191. hud/tools/computer/qwen.py +434 -0
  192. hud/tools/computer/settings.py +37 -0
  193. hud/tools/edit.py +3 -7
  194. hud/tools/executors/base.py +4 -2
  195. hud/tools/executors/pyautogui.py +1 -1
  196. hud/tools/grounding/grounded_tool.py +13 -18
  197. hud/tools/grounding/grounder.py +10 -31
  198. hud/tools/grounding/tests/test_grounded_tool.py +26 -44
  199. hud/tools/jupyter.py +330 -0
  200. hud/tools/playwright.py +18 -3
  201. hud/tools/shell.py +308 -0
  202. hud/tools/tests/test_agent_tool.py +355 -0
  203. hud/tools/tests/test_apply_patch.py +718 -0
  204. hud/tools/tests/test_computer.py +4 -9
  205. hud/tools/tests/test_computer_actions.py +24 -2
  206. hud/tools/tests/test_jupyter_tool.py +181 -0
  207. hud/tools/tests/test_shell.py +596 -0
  208. hud/tools/tests/test_submit.py +85 -0
  209. hud/tools/tests/test_types.py +193 -0
  210. hud/tools/types.py +21 -1
  211. hud/types.py +194 -56
  212. hud/utils/__init__.py +2 -0
  213. hud/utils/env.py +67 -0
  214. hud/utils/hud_console.py +89 -18
  215. hud/utils/mcp.py +15 -58
  216. hud/utils/strict_schema.py +162 -0
  217. hud/utils/tests/test_init.py +1 -2
  218. hud/utils/tests/test_mcp.py +1 -28
  219. hud/utils/tests/test_pretty_errors.py +186 -0
  220. hud/utils/tests/test_tool_shorthand.py +154 -0
  221. hud/utils/tests/test_version.py +1 -1
  222. hud/utils/types.py +20 -0
  223. hud/version.py +1 -1
  224. hud_python-0.5.13.dist-info/METADATA +264 -0
  225. hud_python-0.5.13.dist-info/RECORD +305 -0
  226. {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/WHEEL +1 -1
  227. hud/agents/langchain.py +0 -261
  228. hud/agents/lite_llm.py +0 -72
  229. hud/cli/rl/__init__.py +0 -180
  230. hud/cli/rl/config.py +0 -101
  231. hud/cli/rl/display.py +0 -133
  232. hud/cli/rl/gpu.py +0 -63
  233. hud/cli/rl/gpu_utils.py +0 -321
  234. hud/cli/rl/local_runner.py +0 -595
  235. hud/cli/rl/presets.py +0 -96
  236. hud/cli/rl/remote_runner.py +0 -463
  237. hud/cli/rl/rl_api.py +0 -150
  238. hud/cli/rl/vllm.py +0 -177
  239. hud/cli/rl/wait_utils.py +0 -89
  240. hud/datasets/parallel.py +0 -687
  241. hud/misc/__init__.py +0 -1
  242. hud/misc/claude_plays_pokemon.py +0 -292
  243. hud/otel/__init__.py +0 -35
  244. hud/otel/collector.py +0 -142
  245. hud/otel/config.py +0 -181
  246. hud/otel/context.py +0 -570
  247. hud/otel/exporters.py +0 -369
  248. hud/otel/instrumentation.py +0 -135
  249. hud/otel/processors.py +0 -121
  250. hud/otel/tests/__init__.py +0 -1
  251. hud/otel/tests/test_processors.py +0 -197
  252. hud/rl/README.md +0 -30
  253. hud/rl/__init__.py +0 -1
  254. hud/rl/actor.py +0 -176
  255. hud/rl/buffer.py +0 -405
  256. hud/rl/chat_template.jinja +0 -101
  257. hud/rl/config.py +0 -192
  258. hud/rl/distributed.py +0 -132
  259. hud/rl/learner.py +0 -637
  260. hud/rl/tests/__init__.py +0 -1
  261. hud/rl/tests/test_learner.py +0 -186
  262. hud/rl/train.py +0 -382
  263. hud/rl/types.py +0 -101
  264. hud/rl/utils/start_vllm_server.sh +0 -30
  265. hud/rl/utils.py +0 -524
  266. hud/rl/vllm_adapter.py +0 -143
  267. hud/telemetry/job.py +0 -352
  268. hud/telemetry/replay.py +0 -74
  269. hud/telemetry/tests/test_replay.py +0 -40
  270. hud/telemetry/tests/test_trace.py +0 -63
  271. hud/telemetry/trace.py +0 -158
  272. hud/utils/agent_factories.py +0 -86
  273. hud/utils/async_utils.py +0 -65
  274. hud/utils/group_eval.py +0 -223
  275. hud/utils/progress.py +0 -149
  276. hud/utils/tasks.py +0 -127
  277. hud/utils/tests/test_async_utils.py +0 -173
  278. hud/utils/tests/test_progress.py +0 -261
  279. hud_python-0.4.45.dist-info/METADATA +0 -552
  280. hud_python-0.4.45.dist-info/RECORD +0 -228
  281. {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/entry_points.txt +0 -0
  282. {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,263 @@
1
+ """MCP routing for Environment - tools, prompts, and resources."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from dataclasses import dataclass, field
7
+ from enum import Enum
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ import mcp.types as mcp_types
12
+
13
+ from hud.environment.connection import Connector
14
+
15
+ __all__ = ["LOCAL_CONNECTION", "ConflictResolution", "MCPRouter", "ToolRouter"]
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ LOCAL_CONNECTION = "__local__"
20
+
21
+
22
+ class ConflictResolution(str, Enum):
23
+ """Strategy for resolving name conflicts."""
24
+
25
+ PREFIX = "prefix" # Add connection name as prefix
26
+ FIRST_WINS = "first_wins" # First connection wins
27
+ LAST_WINS = "last_wins" # Last connection wins
28
+ ERROR = "error" # Raise error on conflict
29
+
30
+
31
+ @dataclass
32
+ class MCPRouter:
33
+ """Routes tools, prompts, and resources to local or remote handlers.
34
+
35
+ Builds routing tables during Environment.__aenter__ from local registrations
36
+ and connection caches. Provides get_*_connection() methods to find which
37
+ connection serves a given tool/prompt/resource.
38
+ """
39
+
40
+ conflict_resolution: ConflictResolution = ConflictResolution.PREFIX
41
+
42
+ # Tool routing
43
+ _tools: list[mcp_types.Tool] = field(default_factory=list)
44
+ _tool_routing: dict[str, str] = field(default_factory=dict) # name -> connection
45
+ _local_tool_names: set[str] = field(default_factory=set)
46
+
47
+ # Prompt routing
48
+ _prompts: list[mcp_types.Prompt] = field(default_factory=list)
49
+ _prompt_routing: dict[str, str] = field(default_factory=dict) # name -> connection
50
+
51
+ # Resource routing
52
+ _resources: list[mcp_types.Resource] = field(default_factory=list)
53
+ _resource_routing: dict[str, str] = field(default_factory=dict) # uri -> connection
54
+
55
+ # =========================================================================
56
+ # Tool routing (backwards compatible)
57
+ # =========================================================================
58
+
59
+ @property
60
+ def tools(self) -> list[mcp_types.Tool]:
61
+ return self._tools
62
+
63
+ def is_local(self, name: str) -> bool:
64
+ """Check if tool is local (backwards compat)."""
65
+ return name in self._local_tool_names
66
+
67
+ def get_connection(self, name: str) -> str | None:
68
+ """Get connection name for tool, None if local or not found (backwards compat)."""
69
+ return self.get_tool_connection(name)
70
+
71
+ def get_tool_connection(self, name: str) -> str | None:
72
+ """Get connection name for tool, None if local or not found."""
73
+ conn = self._tool_routing.get(name)
74
+ return None if conn == LOCAL_CONNECTION else conn
75
+
76
+ # =========================================================================
77
+ # Prompt routing
78
+ # =========================================================================
79
+
80
+ @property
81
+ def prompts(self) -> list[mcp_types.Prompt]:
82
+ return self._prompts
83
+
84
+ def get_prompt_connection(self, name: str) -> str | None:
85
+ """Get connection name for prompt, None if local or not found."""
86
+ conn = self._prompt_routing.get(name)
87
+ return None if conn == LOCAL_CONNECTION else conn
88
+
89
+ # =========================================================================
90
+ # Resource routing
91
+ # =========================================================================
92
+
93
+ @property
94
+ def resources(self) -> list[mcp_types.Resource]:
95
+ return self._resources
96
+
97
+ def get_resource_connection(self, uri: str) -> str | None:
98
+ """Get connection name for resource, None if local or not found."""
99
+ conn = self._resource_routing.get(uri)
100
+ return None if conn == LOCAL_CONNECTION else conn
101
+
102
+ # =========================================================================
103
+ # Building routes
104
+ # =========================================================================
105
+
106
+ def clear(self) -> None:
107
+ """Clear all routing tables."""
108
+ self._tools.clear()
109
+ self._tool_routing.clear()
110
+ self._local_tool_names.clear()
111
+ self._prompts.clear()
112
+ self._prompt_routing.clear()
113
+ self._resources.clear()
114
+ self._resource_routing.clear()
115
+
116
+ def build(
117
+ self,
118
+ local_tools: list[mcp_types.Tool],
119
+ connections: dict[str, Connector],
120
+ connection_order: list[str],
121
+ ) -> None:
122
+ """Build tool routing from local tools and connection caches.
123
+
124
+ Local tools always have priority over remote tools.
125
+ Tools starting with '_' are internal and hidden from listing
126
+ (but still callable directly).
127
+ """
128
+ # Clear tool routing only (prompts/resources built separately)
129
+ self._tools.clear()
130
+ self._tool_routing.clear()
131
+ self._local_tool_names.clear()
132
+
133
+ seen: dict[str, str] = {}
134
+
135
+ # Local tools first (always priority)
136
+ for tool in local_tools:
137
+ seen[tool.name] = LOCAL_CONNECTION
138
+ self._tool_routing[tool.name] = LOCAL_CONNECTION
139
+ self._local_tool_names.add(tool.name)
140
+ if not tool.name.startswith("_"):
141
+ self._tools.append(tool)
142
+
143
+ # Remote connections in order
144
+ for conn_name in connection_order:
145
+ if conn_name not in connections:
146
+ continue
147
+ for tool in connections[conn_name].cached_tools:
148
+ name = tool.name
149
+ if name in seen:
150
+ existing = seen[name]
151
+ if existing == LOCAL_CONNECTION:
152
+ continue
153
+ if not self._handle_conflict(name, existing, conn_name):
154
+ continue
155
+ self._tools = [t for t in self._tools if t.name != name]
156
+
157
+ seen[name] = conn_name
158
+ self._tool_routing[name] = conn_name
159
+ if not name.startswith("_"):
160
+ self._tools.append(tool)
161
+
162
+ logger.debug("Router: %d tools (%d local)", len(self._tools), len(self._local_tool_names))
163
+
164
+ def build_prompts(
165
+ self,
166
+ local_prompts: list[mcp_types.Prompt],
167
+ connections: dict[str, Connector],
168
+ ) -> None:
169
+ """Build prompt routing from local prompts and connections.
170
+
171
+ Uses cached prompts from connections (populated during __aenter__).
172
+ """
173
+ self._prompts.clear()
174
+ self._prompt_routing.clear()
175
+
176
+ seen: dict[str, str] = {}
177
+
178
+ # Local prompts first (always priority)
179
+ for prompt in local_prompts:
180
+ seen[prompt.name] = LOCAL_CONNECTION
181
+ self._prompt_routing[prompt.name] = LOCAL_CONNECTION
182
+ self._prompts.append(prompt)
183
+
184
+ # Use cached prompts from each connection (populated during __aenter__)
185
+ results: list[tuple[str, list[mcp_types.Prompt]]] = [
186
+ (conn_name, conn.cached_prompts) for conn_name, conn in connections.items()
187
+ ]
188
+
189
+ # Process results in connection order (dict preserves insertion order)
190
+ for conn_name, remote_prompts in results:
191
+ for prompt in remote_prompts:
192
+ name = prompt.name
193
+ if name in seen:
194
+ existing = seen[name]
195
+ if existing == LOCAL_CONNECTION:
196
+ continue # Local always wins
197
+ if not self._handle_conflict(name, existing, conn_name):
198
+ continue
199
+ # Remove old prompt from list
200
+ self._prompts = [p for p in self._prompts if p.name != name]
201
+
202
+ seen[name] = conn_name
203
+ self._prompt_routing[name] = conn_name
204
+ self._prompts.append(prompt)
205
+
206
+ logger.debug("Router: %d prompts", len(self._prompts))
207
+
208
+ def build_resources(
209
+ self,
210
+ local_resources: list[mcp_types.Resource],
211
+ connections: dict[str, Connector],
212
+ ) -> None:
213
+ """Build resource routing from local resources and connections.
214
+
215
+ Uses cached resources from connections (populated during __aenter__).
216
+ """
217
+ self._resources.clear()
218
+ self._resource_routing.clear()
219
+
220
+ seen: dict[str, str] = {}
221
+
222
+ # Local resources first (always priority)
223
+ for resource in local_resources:
224
+ uri = str(resource.uri)
225
+ seen[uri] = LOCAL_CONNECTION
226
+ self._resource_routing[uri] = LOCAL_CONNECTION
227
+ self._resources.append(resource)
228
+
229
+ # Use cached resources from each connection (populated during __aenter__)
230
+ results: list[tuple[str, list[mcp_types.Resource]]] = [
231
+ (conn_name, conn.cached_resources) for conn_name, conn in connections.items()
232
+ ]
233
+
234
+ # Process results in connection order (dict preserves insertion order)
235
+ for conn_name, remote_resources in results:
236
+ for resource in remote_resources:
237
+ uri = str(resource.uri)
238
+ if uri in seen:
239
+ existing = seen[uri]
240
+ if existing == LOCAL_CONNECTION:
241
+ continue # Local always wins
242
+ if not self._handle_conflict(uri, existing, conn_name):
243
+ continue
244
+ # Remove old resource from list
245
+ self._resources = [r for r in self._resources if str(r.uri) != uri]
246
+
247
+ seen[uri] = conn_name
248
+ self._resource_routing[uri] = conn_name
249
+ self._resources.append(resource)
250
+
251
+ logger.debug("Router: %d resources", len(self._resources))
252
+
253
+ def _handle_conflict(self, name: str, existing: str, new: str) -> bool:
254
+ """Handle remote-to-remote conflict. Returns True to replace existing."""
255
+ if self.conflict_resolution == ConflictResolution.ERROR:
256
+ raise ValueError(f"Conflict: '{name}' in '{existing}' and '{new}'")
257
+ if self.conflict_resolution == ConflictResolution.FIRST_WINS:
258
+ return False
259
+ return self.conflict_resolution == ConflictResolution.LAST_WINS
260
+
261
+
262
+ # Backwards compatibility alias
263
+ ToolRouter = MCPRouter