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
Expand Up @@ -29,7 +29,7 @@ dev = [
"pytest>=9.0.2",
"pytest-asyncio>=1.3.0",
"ruff>=0.14.9",
"ty>=0.0.1a35",
"ty>=0.0.2",
"pytest-cov>=7.0.0",
]
security = [
Expand Down
43 changes: 24 additions & 19 deletions src/mcp_optimizer/toolhive/toolhive_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import os
import time
from functools import wraps
from typing import Any, Awaitable, Callable, Self, TypeVar
from typing import Any, Awaitable, Callable, Self, TypeVar, cast
from urllib.parse import urlparse

import httpx
Expand Down Expand Up @@ -468,7 +468,18 @@ async def wrapper(*args: Any, **kwargs: Any) -> T:
host=self.thv_host,
port=self.thv_port,
)
break
# All retries exhausted - raise immediately
error_msg = (
f"Failed to connect to ToolHive after {self.max_retries} attempts. "
f"Last error: {last_exception}"
)
logger.critical(
"ToolHive connection failure - exiting",
max_retries=self.max_retries,
host=self.thv_host,
last_error=str(last_exception),
)
raise ToolhiveConnectionError(error_msg) from last_exception

# Try to rediscover the port
logger.info(
Expand All @@ -493,20 +504,10 @@ async def wrapper(*args: Any, **kwargs: Any) -> T:
await asyncio.sleep(backoff)
backoff = min(backoff * 2, self.max_backoff)

# All retries exhausted
error_msg = (
f"Failed to connect to ToolHive after {self.max_retries} attempts. "
f"Last error: {last_exception}"
)
logger.critical(
"ToolHive connection failure - exiting",
max_retries=self.max_retries,
host=self.thv_host,
last_error=str(last_exception),
)
raise ToolhiveConnectionError(error_msg) from last_exception
# Should never reach here - all paths should either return or raise
raise RuntimeError("Unexpected code path in _with_retry. Async wrapper failed.")

return wrapper
return cast(Callable[..., Awaitable[T]], wrapper)

async def __aenter__(self) -> Self:
"""Async context manager entry."""
Expand Down Expand Up @@ -579,7 +580,8 @@ async def _list_workloads_impl() -> WorkloadListResponse:

return workload_list

return await self._with_retry(_list_workloads_impl)()
result = await self._with_retry(_list_workloads_impl)()
return cast(WorkloadListResponse, result)

async def get_workload_details(self, workload_name: str) -> Workload:
"""
Expand Down Expand Up @@ -629,7 +631,8 @@ async def _get_workload_details_impl() -> Workload:

return workload

return await self._with_retry(_get_workload_details_impl)()
result = await self._with_retry(_get_workload_details_impl)()
return cast(Workload, result)

async def get_running_mcp_workloads(self) -> list[Workload]:
"""
Expand Down Expand Up @@ -699,7 +702,8 @@ async def _get_registry_impl() -> Registry:

return registry

return await self._with_retry(_get_registry_impl)()
result = await self._with_retry(_get_registry_impl)()
return cast(Registry, result)

async def get_server_from_registry(
self, server_name: str
Expand Down Expand Up @@ -776,7 +780,8 @@ async def _install_server_impl() -> dict:

return result

return await self._with_retry(_install_server_impl)()
result = await self._with_retry(_install_server_impl)()
return cast(dict, result)

async def close(self):
"""Close the HTTP client."""
Expand Down
46 changes: 23 additions & 23 deletions uv.lock

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