Template best practices#123
Template best practices#123openshift-merge-bot[bot] merged 4 commits intoopenshift-assisted:masterfrom
Conversation
|
Skipping CI for Draft Pull Request. |
WalkthroughReplaces the legacy monolithic server with a packaged FastAPI-based MCP implementation: adds the assisted_service_mcp package (settings, logging, auth, MCP server, tools, metrics), updates dev/build configs and tests, removes Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant CLI as uvicorn (python -m assisted_service_mcp.src.main)
participant API as assisted_service_mcp.src.api
participant MCP as AssistedServiceMCPServer
participant Auth as assisted_service_mcp.utils.auth
participant Client as InventoryClient
participant Metrics as assisted_service_mcp.src.metrics
CLI->>API: import api (configure_logging)
API->>MCP: instantiate server (register tools)
MCP->>Auth: bind auth closures (_get_offline_token/_get_access_token)
CLI->>MCP: call server.list_tools_sync()
CLI->>Metrics: initiate_metrics(tool_names)
CLI->>CLI: start uvicorn(host, port)
Note over API,MCP: Incoming HTTP MCP request via selected transport
API->>MCP: dispatch tool call
MCP->>Auth: _get_access_token() -> token
MCP->>Client: InventoryClient(token)
MCP->>Metrics: track_tool_usage(tool)
MCP-->>API: tool response
API-->>Client: HTTP response
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
6bc07da to
050b604
Compare
There was a problem hiding this comment.
Actionable comments posted: 13
♻️ Duplicate comments (1)
tests/test_integration_api.py (1)
1-58: Missing test dependency causes pipeline failure.Same as
test_metrics.py, this test file requiresfastapibut the package is not installed, causingModuleNotFoundError.This is the same dependency issue as in
tests/test_metrics.py. Ensurefastapiis added to test dependencies.
🧹 Nitpick comments (11)
pyrightconfig.json (2)
8-10: Redundant: path listed in both "exclude" and "ignore"."integration_test/performance" is already excluded, so the "ignore" entry is unnecessary. Keep one behavior (exclude vs ignore) intentionally.
Apply this diff to drop the redundant ignore block:
"exclude": [ "integration_test/performance", ".venv", "venv" ], - "ignore": [ - "integration_test/performance" - ],
12-12: Consider enabling library type inference."useLibraryCodeForTypes": false can reduce useful inference for third‑party libs without stubs. Default is true; recommend enabling unless you hit perf/noise issues.
- "useLibraryCodeForTypes": false + "useLibraryCodeForTypes": trueassisted_service_mcp/src/utils/static_net/template.py (1)
124-124: Prefer excluding None when rendering to reduce template branchingUsing exclude_none avoids passing None into the template context and keeps rendered YAML cleaner.
- return yaml.dump(yaml.safe_load(NMSTATE_TEMPLATE.render(**params.model_dump()))) + return yaml.dump( + yaml.safe_load(NMSTATE_TEMPLATE.render(**params.model_dump(exclude_none=True))) + )tests/test_logger_filter.py (1)
4-5: Silence protected member access warning in testsAccessing a protected helper is acceptable in tests; silence pylint.
def filter_text(text: str) -> str: - return SensitiveFormatter._filter(text) + return SensitiveFormatter._filter(text) # pylint: disable=protected-accesstests/test_api.py (1)
18-27: Consider verifying transport-specific behavior beyond attribute existence.While the tests confirm that
appandserverattributes are exposed, they don't verify that the correct transport is actually configured (e.g., by checking server.mcp.stateless_http or inspecting the app type).Consider enhancing the tests to verify transport-specific attributes:
def test_api_uses_sse_when_configured() -> None: api_mod = import_api_with_transport("sse") assert hasattr(api_mod, "app") assert hasattr(api_mod, "server") + # Verify SSE transport is configured (stateful) + assert api_mod.server.mcp.stateless_http is False def test_api_uses_streamable_http_when_configured() -> None: api_mod = import_api_with_transport("streamable-http") assert hasattr(api_mod, "app") assert hasattr(api_mod, "server") + # Verify StreamableHTTP transport is configured (stateless) + assert api_mod.server.mcp.stateless_http is Trueassisted_service_mcp/utils/auth.py (2)
105-105: Use lazy % formatting in logging statements.F-string interpolation in logging functions is less efficient because the string is always evaluated, even when the log level is disabled.
Apply this diff:
- log.error(f"Failed to exchange offline token for access token: {e}") + log.error("Failed to exchange offline token for access token: %s", e) raise RuntimeError(f"Failed to obtain access token from SSO: {e}") from e try: response_data = response.json() access_token = response_data["access_token"] except (KeyError, ValueError) as e: - log.error(f"Invalid SSO response format: {e}") + log.error("Invalid SSO response format: %s", e) raise RuntimeError(Also applies to: 112-112
102-102: Consider making the SSO timeout configurable.The hardcoded 30-second timeout may not be appropriate for all environments (e.g., slow networks, production vs. test).
Consider adding a
SSO_TIMEOUTsetting with a default of 30 seconds, allowing runtime configuration without code changes.tests/test_integration_api.py (1)
20-35: Simplify route checking logic.The route path extraction logic is overly complex with nested
getattrand conditional expressions.Consider this cleaner approach:
def ensure_metrics_route(app) -> None: # type: ignore[no-untyped-def] # Attach /metrics route for the test (normally added in main()) from assisted_service_mcp.src.metrics import ( metrics as metrics_endpoint, ) # pylint: disable=import-outside-toplevel - routes = { - ( - getattr(getattr(r, "path", None), "strip", str)("/") - if hasattr(r, "path") - else getattr(r, "path", None) - ): r - for r in getattr(app, "routes", []) - } + existing_paths = {r.path for r in getattr(app, "routes", []) if hasattr(r, "path")} - if "/metrics" not in routes: + if "/metrics" not in existing_paths: app.add_route("/metrics", metrics_endpoint)tests/test_tools_module.py (1)
18-18: Consider extracting server instantiation to a fixture.Multiple tests instantiate
AssistedServiceMCPServer()for side effects (tool registration). This could be extracted to a pytest fixture to reduce duplication and make the intent clearer.Example:
@pytest.fixture(scope="module") def mcp_server(): """Instantiate MCP server once for tool registration.""" return AssistedServiceMCPServer() @pytest.mark.asyncio async def test_tool_set_cluster_platform_module(mcp_server) -> None: from assisted_service_mcp.src.tools import cluster_tools # ... rest of testAlso applies to: 45-45, 67-67, 91-91
assisted_service_mcp/src/tools/event_tools.py (1)
44-50: Inconsistent token retrieval pattern.
cluster_eventsretrieves the token separately (line 46) before creating the client (line 47), whilehost_eventsretrieves it inline during client creation (line 97). Consider standardizing to one approach for consistency.Recommend the inline pattern as it's more concise:
@track_tool_usage() async def cluster_events( get_access_token_func: Callable[[], str], cluster_id: Annotated[ str, Field(description="The unique identifier of the cluster to get events for."), ], ) -> str: """...""" log.info("Retrieving events for cluster_id: %s", cluster_id) try: - access_token = get_access_token_func() - client = InventoryClient(access_token) + client = InventoryClient(get_access_token_func()) result = await client.get_events(cluster_id=cluster_id) log.info("Successfully retrieved events for cluster %s", cluster_id) return resultAlso applies to: 95-104
assisted_service_mcp/src/settings.py (1)
146-177: Remove unusedvalidate_configinassisted_service_mcp/src/settings.py. It duplicates Pydantic constraints on MCP_PORT, LOGGING_LEVEL, and TRANSPORT and is never invoked.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (44)
Dockerfile(2 hunks)Makefile(2 hunks)README.md(2 hunks)assisted_service_mcp/__init__.py(1 hunks)assisted_service_mcp/src/__init__.py(1 hunks)assisted_service_mcp/src/api.py(1 hunks)assisted_service_mcp/src/main.py(1 hunks)assisted_service_mcp/src/mcp.py(1 hunks)assisted_service_mcp/src/service_client/assisted_service_api.py(3 hunks)assisted_service_mcp/src/service_client/exceptions.py(1 hunks)assisted_service_mcp/src/service_client/helpers.py(1 hunks)assisted_service_mcp/src/service_client/logger.py(1 hunks)assisted_service_mcp/src/settings.py(1 hunks)assisted_service_mcp/src/tools/__init__.py(1 hunks)assisted_service_mcp/src/tools/cluster_tools.py(1 hunks)assisted_service_mcp/src/tools/download_tools.py(1 hunks)assisted_service_mcp/src/tools/event_tools.py(1 hunks)assisted_service_mcp/src/tools/host_tools.py(1 hunks)assisted_service_mcp/src/tools/network_tools.py(1 hunks)assisted_service_mcp/src/tools/shared_helpers.py(1 hunks)assisted_service_mcp/src/tools/version_tools.py(1 hunks)assisted_service_mcp/src/utils/static_net/config.py(2 hunks)assisted_service_mcp/src/utils/static_net/template.py(5 hunks)assisted_service_mcp/utils/__init__.py(1 hunks)assisted_service_mcp/utils/auth.py(1 hunks)assisted_service_mcp/utils/helpers.py(1 hunks)integration_test/performance/README.md(1 hunks)pyproject.toml(3 hunks)pyrightconfig.json(1 hunks)server.py(0 hunks)service_client/logger.py(0 hunks)tests/test_api.py(1 hunks)tests/test_assisted_service_api.py(4 hunks)tests/test_auth.py(1 hunks)tests/test_helpers.py(1 hunks)tests/test_integration_api.py(1 hunks)tests/test_logger_filter.py(1 hunks)tests/test_mcp.py(1 hunks)tests/test_metrics.py(1 hunks)tests/test_server.py(0 hunks)tests/test_settings.py(1 hunks)tests/test_shared_helpers.py(1 hunks)tests/test_static_net.py(3 hunks)tests/test_tools_module.py(1 hunks)
💤 Files with no reviewable changes (3)
- service_client/logger.py
- server.py
- tests/test_server.py
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: carbonin
PR: openshift-assisted/assisted-service-mcp#111
File: pyproject.toml:9-9
Timestamp: 2025-09-25T19:01:36.933Z
Learning: The `mcp` Python package (mcp>=1.15.0) includes FastMCP functionality and provides the same import path `from mcp.server.fastmcp import FastMCP` for backward compatibility with the standalone `fastmcp` package. This allows drop-in replacement when migrating from `fastmcp>=2.8.0` to `mcp>=1.15.0` without requiring code changes.
🧬 Code graph analysis (22)
assisted_service_mcp/src/tools/host_tools.py (3)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (2)
InventoryClient(26-540)update_host(453-481)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
assisted_service_mcp/src/api.py (2)
assisted_service_mcp/src/mcp.py (1)
AssistedServiceMCPServer(31-168)assisted_service_mcp/src/service_client/logger.py (1)
configure_logging(132-179)
assisted_service_mcp/src/service_client/assisted_service_api.py (3)
assisted_service_mcp/src/service_client/exceptions.py (1)
sanitize_exceptions(24-58)assisted_service_mcp/src/service_client/helpers.py (1)
Helpers(7-38)assisted_service_mcp/src/settings.py (1)
get_setting(186-195)
tests/test_integration_api.py (1)
assisted_service_mcp/src/metrics/metrics.py (1)
metrics(69-71)
tests/test_shared_helpers.py (2)
assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)assisted_service_mcp/src/service_client/assisted_service_api.py (1)
list_infra_envs(218-236)
assisted_service_mcp/src/tools/event_tools.py (2)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (1)
get_events(156-197)
tests/test_helpers.py (1)
assisted_service_mcp/src/service_client/helpers.py (1)
Helpers(7-38)
assisted_service_mcp/src/main.py (2)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)initiate_metrics(44-48)assisted_service_mcp/src/mcp.py (1)
list_tools_sync(156-168)
assisted_service_mcp/utils/auth.py (2)
assisted_service_mcp/src/settings.py (1)
get_setting(186-195)tests/test_auth.py (1)
get_context(22-23)
tests/test_tools_module.py (8)
assisted_service_mcp/src/mcp.py (1)
AssistedServiceMCPServer(31-168)assisted_service_mcp/src/service_client/assisted_service_api.py (14)
update_cluster(330-368)create_cluster(239-278)install_cluster(371-386)get_events(156-197)list_infra_envs(218-236)get_infra_env_download_url(516-540)get_presigned_for_cluster_credentials(484-513)update_host(453-481)add_operator_bundle_to_cluster(423-450)get_operator_bundles(410-420)get_infra_env(200-215)get_openshift_versions(389-407)create_infra_env(281-303)update_infra_env(306-327)assisted_service_mcp/src/tools/cluster_tools.py (5)
set_cluster_platform(290-330)set_cluster_vips(232-286)create_cluster(104-228)install_cluster(334-372)set_cluster_ssh_key(376-439)assisted_service_mcp/src/tools/event_tools.py (2)
cluster_events(12-53)host_events(57-112)assisted_service_mcp/src/tools/download_tools.py (2)
cluster_iso_download_url(14-95)cluster_credentials_download_url(99-167)assisted_service_mcp/src/tools/host_tools.py (1)
set_host_role(13-65)assisted_service_mcp/src/tools/version_tools.py (3)
add_operator_bundle_to_cluster(86-141)list_operator_bundles(49-82)list_versions(13-45)assisted_service_mcp/src/tools/network_tools.py (4)
validate_nmstate_yaml(22-55)alter_static_network_config_nmstate_for_host(106-175)list_static_network_config(179-221)generate_nmstate_yaml(59-102)
tests/test_static_net.py (2)
assisted_service_mcp/src/utils/static_net/config.py (3)
remove_static_host_config_by_index(23-38)add_or_replace_static_host_config_yaml(41-70)validate_and_parse_nmstate(96-108)assisted_service_mcp/src/utils/static_net/template.py (2)
generate_nmstate_from_template(121-124)NMStateTemplateParams(102-118)
tests/test_assisted_service_api.py (2)
assisted_service_mcp/src/service_client/assisted_service_api.py (1)
InventoryClient(26-540)assisted_service_mcp/src/service_client/exceptions.py (1)
AssistedServiceAPIError(15-16)
assisted_service_mcp/src/tools/cluster_tools.py (4)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (8)
InventoryClient(26-540)get_cluster(116-140)list_clusters(143-153)create_cluster(239-278)create_infra_env(281-303)update_cluster(330-368)install_cluster(371-386)update_infra_env(306-327)assisted_service_mcp/src/service_client/helpers.py (1)
Helpers(7-38)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
assisted_service_mcp/src/tools/shared_helpers.py (2)
assisted_service_mcp/src/service_client/assisted_service_api.py (2)
InventoryClient(26-540)list_infra_envs(218-236)tests/test_assisted_service_api.py (1)
client(32-37)
tests/test_mcp.py (1)
assisted_service_mcp/src/mcp.py (2)
AssistedServiceMCPServer(31-168)list_tools_sync(156-168)
assisted_service_mcp/src/tools/version_tools.py (3)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (3)
get_openshift_versions(389-407)get_operator_bundles(410-420)add_operator_bundle_to_cluster(423-450)tests/test_tools_module.py (2)
to_str(444-445)to_str(488-489)
tests/test_metrics.py (1)
assisted_service_mcp/src/metrics/metrics.py (3)
metrics(69-71)initiate_metrics(44-48)track_tool_usage(51-65)
tests/test_logger_filter.py (1)
assisted_service_mcp/src/service_client/logger.py (2)
SensitiveFormatter(15-76)_filter(28-63)
assisted_service_mcp/src/tools/network_tools.py (5)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (4)
InventoryClient(26-540)get_infra_env(200-215)update_infra_env(306-327)list_infra_envs(218-236)assisted_service_mcp/src/utils/static_net/template.py (2)
NMStateTemplateParams(102-118)generate_nmstate_from_template(121-124)assisted_service_mcp/src/utils/static_net/config.py (3)
add_or_replace_static_host_config_yaml(41-70)remove_static_host_config_by_index(23-38)validate_and_parse_nmstate(96-108)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
assisted_service_mcp/src/mcp.py (4)
assisted_service_mcp/utils/auth.py (2)
get_offline_token(11-46)get_access_token(52-118)assisted_service_mcp/src/tools/cluster_tools.py (5)
cluster_info(14-55)list_clusters(59-100)create_cluster(104-228)set_cluster_vips(232-286)install_cluster(334-372)assisted_service_mcp/src/tools/network_tools.py (4)
validate_nmstate_yaml(22-55)generate_nmstate_yaml(59-102)alter_static_network_config_nmstate_for_host(106-175)list_static_network_config(179-221)assisted_service_mcp/src/utils/static_net/template.py (1)
NMStateTemplateParams(102-118)
assisted_service_mcp/src/tools/download_tools.py (3)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (3)
list_infra_envs(218-236)get_infra_env_download_url(516-540)get_presigned_for_cluster_credentials(484-513)assisted_service_mcp/utils/helpers.py (1)
format_presigned_url(11-36)
tests/test_auth.py (1)
assisted_service_mcp/utils/auth.py (2)
get_offline_token(11-46)get_access_token(52-118)
🪛 GitHub Actions: Pyright
tests/test_integration_api.py
[error] 4-4: pyright: Import 'fastapi.testclient' could not be resolved (reportMissingImports)
[error] 41-41: pyright: Cannot access attribute 'app' for class 'object' (reportAttributeAccessIssue)
[error] 54-54: pyright: Cannot access attribute 'app' for class 'object' (reportAttributeAccessIssue)
tests/test_metrics.py
[error] 2-2: pyright: Import 'fastapi' could not be resolved (reportMissingImports)
[error] 3-3: pyright: Import 'fastapi.testclient' could not be resolved (reportMissingImports)
🪛 GitHub Actions: Python linter
assisted_service_mcp/src/service_client/assisted_service_api.py
[warning] 20-20: C0411: first party import "assisted_service_mcp.src.metrics.metrics.API_CALL_LATENCY" should be placed before local imports "logger.log", "exceptions.sanitize_exceptions", "helpers.Helpers" (wrong-import-order)
[warning] 21-21: C0411: first party import "assisted_service_mcp.src.settings.get_setting" should be placed before local imports "logger.log", "exceptions.sanitize_exceptions", "helpers.Helpers" (wrong-import-order)
tests/test_integration_api.py
[error] 4-4: E0401: Unable to import 'fastapi.testclient' (import-error)
assisted_service_mcp/utils/auth.py
[error] 49-49: C0413: Import "from typing import Callable" should be placed at the top of the module (wrong-import-position)
[warning] 105-105: W1203: Use lazy % formatting in logging functions (logging-fstring-interpolation)
[warning] 112-112: W1203: Use lazy % formatting in logging functions (logging-fstring-interpolation)
[error] 8-8: C0411: standard import "typing.Any" should be placed before third party import "requests" and first party imports "assisted_service_mcp.src.service_client.logger.log", "assisted_service_mcp.src.settings.get_setting" (wrong-import-order)
[error] 49-49: C0411: standard import "typing.Callable" should be placed before third party import "requests" and first party imports "assisted_service_mcp.src.service_client.logger.log", "assisted_service_mcp.src.settings.get_setting" (wrong-import-order)
tests/test_tools_module.py
[warning] 10-10: C0415: Import outside toplevel (assisted_service_mcp.src.tools.cluster_tools) (import-outside-toplevel)
[warning] 11-11: C0415: Import outside toplevel (assisted_service_mcp.src.mcp.AssistedServiceMCPServer) (import-outside-toplevel)
[warning] 37-37: C0415: Import outside toplevel (assisted_service_mcp.src.tools.cluster_tools) (import-outside-toplevel)
[warning] 38-38: C0415: Import outside toplevel (assisted_service_mcp.src.mcp.AssistedServiceMCPServer) (import-outside-toplevel)
[warning] 64-64: C0415: Import outside toplevel (assisted_service_mcp.src.tools.cluster_tools) (import-outside-toplevel)
[warning] 65-65: C0415: Import outside toplevel (assisted_service_mcp.src.mcp.AssistedServiceMCPServer) (import-outside-toplevel)
[warning] 83-83: C0415: Import outside toplevel (assisted_service_mcp.src.tools.cluster_tools) (import-outside-toplevel)
[warning] 84-84: C0415: Import outside toplevel (assisted_service_mcp.src.mcp.AssistedServiceMCPServer) (import-outside-toplevel)
[warning] 108-108: C0415: Import outside topevel (assisted_service_mcp.src.tools.event_tools) (import-outside-toplevel)
[warning] 109-109: C0415: Import outside toplevel (assisted_service_mcp.src.mcp.AssistedServiceMCPServer) (import-outside-toplevel)
[warning] 131-131: C0415: Import outside toplevel (assisted_service_mcp.src.tools.event_tools) (import-outside-toplevel)
[warning] 132-132: C0415: Import outside toplevel (assisted_service_mcp.src.mcp.AssistedServiceMCPServer) (import-outside-toplevel)
[warning] 154-154: C0415: Import outside toplevel (assisted_service_mcp.src.tools.download_tools) (import-outside-toplevel)
[warning] 155-155: C0415: Import outside toplevel (assisted_service_mcp.src.mcp.AssistedServiceMCPServer) (import-outside-toplevel)
[warning] 193-193: C0415: Import outside toplevel (assisted_service_mcp.src.tools.download_tools) (import-outside-toplevel)
[warning] 194-194: C0415: Import outside toplevel (assisted_service_mcp.src.mcp.AssistedServiceMCPServer) (import-outside-toplevel)
[warning] 226-226: C0415: Import outside toplevel (assisted_service_mcp.src.tools.host_tools) (import-outside-toplevel)
[warning] 227-227: C0415: Import outside toplevel (assisted_service_mcp.src.mcp.AssistedServiceMCPServer) (import-outside-toplevel)
[warning] 257-257: C0415: Import outside toplevel (assisted_service_mcp.src.tools.version_tools) (import-outside-toplevel)
[warning] 258-258: C0415: Import outside toplevel (assisted_service_mcp.src.mcp.AssistedServiceMCPServer) (import-outside-toplevel)
[warning] 284-284: C0415: Import outside toplevel (assisted_service_mcp.src.tools.version_tools) (import-outside-toplevel)
[warning] 285-285: C0415: Import outside toplevel (assisted_service_mcp.src.mcp.AssistedServiceMCPServer) (import-outside-toplevel)
[warning] 308-308: C0415: Import outside toplevel (assisted_service_mcp.src.tools.network_tools) (import-outside-toplevel)
[warning] 309-309: C0415: Import outside toplevel (assisted_service_mcp.src.mcp.AssistedServiceMCPServer) (import-outside-toplevel)
[warning] 323-323: C0415: Import outside toplevel (assisted_service_mcp.src.tools.network_tools) (import-outside-toplevel)
[warning] 324-324: C0415: Import outside toplevel (assisted_service_mcp.src.mcp.AssistedServiceMCPServer) (import-outside-toplevel)
[warning] 349-349: C0415: Import outside toplevel (assisted_service_mcp.src.tools.network_tools) (import-outside-toplevel)
[warning] 350-350: C0415: Import outside toplevel (assisted_service_mcp.src.mcp.AssistedServiceMCPServer) (import-outside-toplevel)
[warning] 375-375: C0415: Import outside toplevel (assisted_service_mcp.src.tools.network_tools) (import-outside-toplevel)
[warning] 376-376: C0415: Import outside toplevel (assisted_service_mcp.src.mcp.AssistedServiceMCPServer) (import-outside-toplevel)
[warning] 391-391: C0415: Import outside toplevel (assisted_service_mcp.src.tools.version_tools) (import-outside-toplevel)
[warning] 392-392: C0415: Import outside toplevel (assisted_service_mcp.src.mcp.AssistedServiceMCPServer) (import-outside-toplevel)
[warning] 407-407: C0415: Import outside toplevel (assisted_service_mcp.src.tools.network_tools) (import-outside-toplevel)
[warning] 408-408: C0415: Import outside toplevel (assisted_service_mcp.src.mcp.AssistedServiceMCPServer) (import-outside-toplevel)
[warning] 412-412: C0415: Import outside toplevel (assisted_service_mcp.src.utils.static_net.template.NMStateTemplateParams, assisted_service_mcp.src.utils.static_net.template.EthernetInterfaceParams) (import-outside-toplevel)
[warning] 412-412: C0415: Import outside toplevel (assisted_service_mcp.src.mcp.AssistedServiceMCPServer) (import-outside-toplevel)
[warning] 435-435: C0415: Import outside toplevel (assisted_service_mcp.src.tools.cluster_tools) (import-outside-toplevel)
[warning] 436-436: C0415: Import outside toplevel (assisted_service_mcp.src.mcp.AssistedServiceMCPServer) (import-outside-toplevel)
[warning] 481-481: C0415: Import outside toplevel (assisted_service_mcp.src.tools.cluster_tools) (import-outside-toplevel)
[warning] 482-482: C0415: Import outside toplevel (assisted_service_mcp.src.mcp.AssistedServiceMCPServer) (import-outside-toplevel)
[warning] 481-481: C0415: Import outside toplevel (assisted_service_mcp.src.tools.cluster_tools) (import-outside-toplevel)
[warning] 482-482: C0415: Import outside toplevel (assisted_service_mcp.src.mcp.AssistedServiceMCPServer) (import-outside-toplevel)
assisted_service_mcp/src/settings.py
[warning] 146-146: W0621: Redefining name 'settings' from outer scope (line 180) (redefined-outer-name)
[error] 183-183: C0413: Import "from typing import Any" should be placed at the top of the module (wrong-import-position)
[error] 183-183: C0411: standard import "typing.Any" should be placed before third party imports "dotenv.load_dotenv", "pydantic.Field", "pydantic_settings.BaseSettings" (wrong-import-order)
[warning] 183-183: C0412: Imports from package typing are not grouped (ungrouped-imports)
assisted_service_mcp/src/tools/cluster_tools.py
[error] 416-416: C0415: Import outside toplevel (assisted_service_mcp.src.tools.shared_helpers._get_cluster_infra_env_id) (import-outside-toplevel)
tests/test_metrics.py
[error] 2-2: E0401: Unable to import 'fastapi' (import-error)
tests/test_logger_filter.py
[warning] 5-5: Protected member access in test (protected-access)
assisted_service_mcp/src/tools/network_tools.py
[warning] 23-23: W0613: Unused argument 'get_access_token_func' (unused-argument)
[warning] 60-60: W0613: Unused argument 'get_access_token_func' (unused-argument)
tests/test_api.py
[warning] 1-1: Unknown linting issue reported in test file
assisted_service_mcp/src/service_client/logger.py
[error] 87-87: C0415: Import outside toplevel (assisted_service_mcp.src.settings.settings) (import-outside-toplevel)
[error] 90-90: E1101: Instance of 'FieldInfo' has no 'upper' member (no-member)
[error] 144-144: C0415: Import outside toplevel (assisted_service_mcp.src.settings.settings) (import-outside-toplevel)
[warning] 150-150: W0603: Using the global statement (global-statement)
assisted_service_mcp/src/mcp.py
[error] 28-28: C0411: standard import "typing.Any" should be placed before third party import "mcp.server.fastmcp.FastMCP" and first party imports "assisted_service_mcp.src.service_client.logger.log", "assisted_service_mcp.utils.auth.get_offline_token", "assisted_service_mcp.src.settings.settings", "assisted_service_mcp.src.tools.cluster_tools" (wrong-import-order)
🪛 GitHub Actions: Ruff
assisted_service_mcp/utils/auth.py
[error] 49-49: Ruff: E402 module level import not at top of file.
assisted_service_mcp/src/settings.py
[error] 183-183: Ruff: E402 module level import not at top of file.
🪛 GitHub Actions: Unit Tests
tests/test_integration_api.py
[error] 1-1: ModuleNotFoundError: No module named 'fastapi' during test collection. Ensure 'fastapi' is installed and listed in dependencies.
tests/test_metrics.py
[error] 1-1: ModuleNotFoundError: No module named 'fastapi' during test collection. Ensure 'fastapi' is installed and listed in dependencies.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Red Hat Konflux / assisted-service-mcp-saas-main-on-pull-request
🔇 Additional comments (49)
pyrightconfig.json (1)
1-13: Enable library type checking; drop extraPathspyrightconfig.json "excludeTests": false, - "useLibraryCodeForTypes": false + "useLibraryCodeForTypes": trueassisted_service_mcp/src/tools/__init__.py (1)
1-1: LGTM!Standard package initializer with clear documentation.
assisted_service_mcp/utils/__init__.py (1)
1-1: LGTM!Standard package initializer with appropriate documentation.
assisted_service_mcp/src/service_client/helpers.py (1)
19-19: LGTM!The updated docstring now accurately reflects that
platformisOptional[str]and documents the default behavior, aligning with the function signature.assisted_service_mcp/src/__init__.py (1)
1-1: LGTM!Standard package initializer.
assisted_service_mcp/src/service_client/exceptions.py (1)
12-12: LGTM!The relative import is appropriate for the new package structure and follows Python best practices for intra-package imports.
assisted_service_mcp/__init__.py (1)
1-6: LGTM!Standard Python package initialization with proper
__version__attribute that matches the version inpyproject.toml.tests/test_helpers.py (1)
6-6: LGTM!Import path correctly updated to reflect the new package structure.
pyproject.toml (2)
18-19: LGTM!The additions of
pydantic-settingsandpython-dotenvalign with the new centralized configuration system introduced inassisted_service_mcp/src/settings.py.
52-52: LGTM!Adding
integration_testto the ignore-paths is appropriate for excluding the integration test directory from linting.integration_test/performance/README.md (1)
22-22: LGTM: Updated entrypoint reflects new package layoutThe module-based invocation matches the refactor to assisted_service_mcp.src.main.
tests/test_static_net.py (2)
12-27: LGTM! Import paths updated for new package structure.The import paths correctly reflect the reorganized module structure under
assisted_service_mcp.src.utils.static_net.
85-85: LGTM! Exception type correctly updated to IndexError.The test expectations now correctly expect
IndexErrorinstead ofValueErrorfor out-of-range index operations, aligning with the implementation changes inconfig.pywhereremove_static_host_config_by_indexraisesIndexErrorfor invalid indexes.Also applies to: 95-95
Makefile (2)
18-18: LGTM! Updated to use module-based entry point.The
run-localtarget now correctly invokes the new module entry pointpython -m assisted_service_mcp.src.main, consistent with the PR's modular architecture refactoring.
33-33: LGTM! Coverage scope updated for package reorganization.The test coverage now targets
service_clientandassisted_service_mcppackages, correctly excluding the removedservermodule.README.md (1)
32-32: LGTM! Documentation updated for new entry point.The README correctly documents the new module-based entry point paths for both VSCode configuration and SSE invocation, aligning with the PR's modular architecture.
Also applies to: 46-46
tests/test_shared_helpers.py (1)
1-28: LGTM! Comprehensive test coverage for the helper function.The tests appropriately cover the three key scenarios for
_get_cluster_infra_env_id:
- Successful retrieval with valid InfraEnv
- Empty InfraEnv list (raises ValueError)
- Missing id field (raises ValueError)
The use of
AsyncMockandpytest.mark.asynciois correct for testing the async helper function.Dockerfile (1)
14-14: LGTM! Dockerfile updated for modular package structure.The changes correctly:
- Simplify the COPY directive to include the entire
assisted_service_mcppackage- Update the CMD to invoke the module-based entry point
python -m assisted_service_mcp.src.mainThese align with the PR's architectural refactoring.
Also applies to: 25-25
assisted_service_mcp/src/tools/shared_helpers.py (1)
7-41: LGTM! Well-implemented internal helper with robust error handling.The
_get_cluster_infra_env_idhelper function:
- Properly handles edge cases (no InfraEnvs, missing IDs, multiple InfraEnvs)
- Provides informative logging for debugging
- Has clear error messages
- Is well-documented with comprehensive docstring
The function is correctly marked as internal with the
_prefix and shared betweenset_host_roleandset_cluster_ssh_keytools.assisted_service_mcp/src/service_client/assisted_service_api.py (1)
43-44: LGTM! Centralized configuration via get_setting.The migration from direct
os.environaccess toget_setting()provides:
- Centralized configuration management
- Type-safe settings via pydantic
- Better testability (settings can be patched in tests)
Reading configuration at construction time (lines 43-44) is a good pattern that enables test patching while maintaining immutability during the client's lifetime.
Also applies to: 71-71
tests/test_mcp.py (1)
1-37: LGTM! Effective smoke test for MCP server initialization.The test provides good coverage of the MCP server's core functionality:
- Dynamic import prevents unintended side effects during test discovery
- Verifies auth closures (
_get_offline_token,_get_access_token) are properly created- Confirms tool registration via
list_tools_sync()- Validates that all expected core tools are registered
This is a valuable integration-level test that ensures the server wires up correctly.
tests/test_auth.py (4)
9-24: LGTM! Helper classes effectively mock the MCP context.The
_ReqCtxand_MCPhelper classes properly simulate the MCP context structure needed for testing authentication utilities. The use oftypes.SimpleNamespaceprovides a lightweight mock that matches the expected interface.
26-42: LGTM! Tests comprehensively cover offline token retrieval logic.The three test functions thoroughly verify:
- Environment variable takes precedence over header value
- Fallback to header when environment variable is None
- RuntimeError is raised when neither source provides a token
This aligns with the implementation in
assisted_service_mcp/utils/auth.py(lines 10-45).
45-47: LGTM! Access token extraction from Authorization header is correctly tested.The test verifies extraction of the bearer token from the "Authorization" header, matching the implementation logic in
assisted_service_mcp/utils/auth.py(lines 51-117).
50-66: LGTM! Offline token-based access token retrieval is thoroughly tested.The test verifies:
- POST request is made to SSO_URL with correct parameters
- Response is parsed for access_token
- Mock assertions confirm the request was issued
The test properly patches both OFFLINE_TOKEN and SSO_URL settings, and verifies the complete OAuth flow.
assisted_service_mcp/src/utils/static_net/config.py (3)
34-36: LGTM! More appropriate exception type for index validation.Changing from
ValueErrortoIndexErrorfor out-of-range index deletion is semantically correct and consistent with Python conventions for index-related errors.
76-77: LGTM! Explicit validation improves error handling.Adding explicit validation for the "interfaces" key with a descriptive error message is a valuable improvement. This provides clearer feedback when the NMState YAML is malformed.
78-87: LGTM! Stricter filtering prevents invalid interface entries.The updated list comprehension (line 84) now correctly filters out interfaces that are missing either "mac-address" or "name", ensuring only complete interface entries are included. The subsequent validation (lines 86-87) ensures at least one valid interface exists.
This is an essential improvement that prevents incomplete interface data from being accepted.
tests/test_api.py (1)
6-15: LGTM! Dynamic module reloading properly isolates transport configurations.The
import_api_with_transporthelper effectively:
- Sets the TRANSPORT environment variable via MonkeyPatch
- Reloads the settings module to apply the change
- Clears and reimports the api module to reflect new settings
This approach correctly tests transport-specific initialization without cross-test contamination.
assisted_service_mcp/src/api.py (2)
11-15: Consider potential issues with module-level initialization.Calling
configure_logging()and instantiatingAssistedServiceMCPServer()at module level means these operations execute on every import, including during testing. This could:
- Make tests harder to isolate (settings may already be loaded)
- Cause import-time failures if dependencies aren't available
- Create unexpected side effects during test discovery
While the current test suite (tests/test_api.py) uses dynamic reloading to handle this, consider whether a factory function approach would be more maintainable:
def create_app(transport: str | None = None) -> tuple[FastAPI, AssistedServiceMCPServer]: """Create and configure the application.""" configure_logging() server = AssistedServiceMCPServer() transport_value = transport or getattr(settings, "TRANSPORT", "sse") if transport_value and str(transport_value).lower() == "streamable-http": app = server.mcp.streamable_http_app() log.info("Using StreamableHTTP transport (stateless)") else: app = server.mcp.sse_app() log.info("Using SSE transport (stateful)") return app, server # For production use app, server = create_app()This would make testing easier and prevent import-time side effects.
18-24: LGTM! Transport selection logic handles both SSE and StreamableHTTP.The transport selection properly:
- Retrieves TRANSPORT from settings with "sse" as default
- Uses case-insensitive comparison
- Logs which transport is selected
- Exposes the appropriate app type
assisted_service_mcp/src/main.py (2)
10-43: LGTM! Well-structured main entry point with proper error handling.The main() function correctly:
- Logs startup configuration details (TRANSPORT, HOST, PORT)
- Initializes metrics with tool names via
server.list_tools_sync()- Registers the /metrics endpoint before starting the server
- Uses uvicorn with settings-based configuration
- Handles KeyboardInterrupt for graceful shutdown
- Catches unexpected exceptions with logging and re-raise
- Ensures shutdown logging via finally block
The startup sequence is logical and the error handling is comprehensive.
45-46: LGTM! Proper entry point guard.The
if __name__ == "__main__"guard ensures main() only runs when the module is executed directly, not when imported as a library.assisted_service_mcp/utils/helpers.py (2)
7-8: LGTM! ZERO_DATETIME constant properly defined.Using
datetime(1, 1, 1, tzinfo=timezone.utc)as a constant for zero datetime is appropriate for filtering out meaningless/default timestamp values.
11-36: LGTM! Presigned URL formatting handles optional expiration correctly.The function properly:
- Always includes the "url" field
- Conditionally includes "expires_at" only when it exists and is meaningful (not ZERO_DATETIME)
- Formats the timestamp as ISO-8601 with "Z" suffix for UTC (replacing "+00:00")
This clean formatting approach prevents cluttering the output with meaningless expiration timestamps.
assisted_service_mcp/src/tools/host_tools.py (3)
12-27: LGTM! Function signature with comprehensive parameter documentation.The function properly:
- Uses
@track_tool_usage()decorator for metrics- Receives
get_access_token_funcas the first parameter for dependency injection- Uses
AnnotatedwithFielddescriptions for all parameters- Leverages
Literaltype for theroleparameter to restrict valuesThe parameter documentation is clear and provides helpful context for users.
29-55: LGTM! Excellent tool docstring following best practices.The docstring is well-structured with:
- Description: Clear explanation of the function's purpose and role types
- Examples: Multiple practical usage examples including HA cluster setup
- Prerequisites: Lists required preconditions (discovered host, host ID, cluster with infra env)
- Related tools: References complementary tools (cluster_info, host_events, cluster_iso_download_url)
- Returns: Describes the return value format
This aligns perfectly with the tool documentation refactor mentioned in the PR objectives (Commit 2).
Based on PR objectives
56-65: LGTM! Implementation correctly uses shared helper and client.The implementation:
- Logs the operation with relevant parameters
- Creates an InventoryClient with the access token
- Retrieves the InfraEnv ID using the shared helper
_get_cluster_infra_env_id- Updates the host with the specified role via
client.update_host- Logs success and returns the formatted result
The use of the shared helper (from
assisted_service_mcp/src/tools/shared_helpers.py) promotes code reuse and consistency.tests/test_assisted_service_api.py (3)
12-13: LGTM! Imports updated for new package structure.The import paths have been correctly updated from the old structure to the new
assisted_service_mcp.src.service_clientpath for bothInventoryClientandAssistedServiceAPIError.
61-70: LGTM! Settings patch targets updated for new module structure.The test now patches settings attributes directly using the new module path
assisted_service_mcp.src.settings.settingsinstead of manipulating environment variables. This is consistent with the centralized settings approach introduced in the PR.
116-118: LGTM! Consistent settings patching throughout test suite.All occurrences of settings patches have been updated to use the new module path pattern. This ensures tests work correctly with the new settings module.
Also applies to: 141-141
tests/test_settings.py (2)
6-16: LGTM! Helper correctly reloads settings with environment overrides.The
reload_settings_with_envhelper properly applies environment patches within theMonkeyPatch().context()and reloads the settings module while the patches are active.
19-53: LGTM! Comprehensive settings validation tests.The tests cover:
- Default values for all major configuration fields
- Environment variable overrides
- Validation error handling for invalid transport values
This provides good coverage of the settings module's behavior.
tests/test_tools_module.py (1)
1-514: LGTM! Comprehensive test coverage for MCP tools.The test suite covers:
- Cluster operations: set_platform, set_vips, create_cluster (including SNO validation), install_cluster
- Event retrieval: cluster_events, host_events
- Download URLs: cluster_iso_download_url, cluster_credentials_download_url
- Host management: set_host_role
- Operators/versions: add_operator_bundle, list_operator_bundles, list_versions
- Network tools: validate_nmstate_yaml, generate_nmstate_yaml, alter_static_network_config, list_static_network_config
- SSH key updates with partial failure handling
All tests properly mock dependencies and verify expected behavior.
Note: The
import-outside-toplevelwarnings are intentional for test isolation and can be safely ignored.assisted_service_mcp/src/tools/event_tools.py (1)
11-112: LGTM! Well-structured event retrieval tools.Both functions:
- Have comprehensive docstrings with examples, prerequisites, and related tools
- Properly use type hints with
AnnotatedandFieldfor parameter descriptions- Include appropriate logging at info/error levels
- Are decorated with
@track_tool_usage()for metrics- Handle errors gracefully by logging and re-raising
assisted_service_mcp/src/tools/download_tools.py (1)
13-167: LGTM! Well-documented download URL tools.Both functions have:
- Comprehensive docstrings with examples, prerequisites, and related tools
- Proper error handling with logging
@track_tool_usage()decoration for metrics- Type hints with
AnnotatedandFieldfor parameter descriptionsThe only concern is the inconsistent return format in
cluster_iso_download_urlflagged above.tests/test_integration_api.py (1)
10-12: MonkeyPatch context concern is unfounded.importlib.reload(settings_mod)is called inside thewith pytest.MonkeyPatch().context()block, so the patchedTRANSPORTenv var is active during reload.Likely an incorrect or invalid review comment.
assisted_service_mcp/src/mcp.py (2)
121-150: Token injection wrapper looks correctSignature surgery and injection are clean; preserves tool doc/signature for clients.
156-168: Synchronous tool listing guard — LGTMProperly avoids nesting event loops and falls back to asyncio.run when safe.
assisted_service_mcp/src/service_client/assisted_service_api.py
Outdated
Show resolved
Hide resolved
| client = InventoryClient(get_access_token_func()) | ||
| infra_envs = await client.list_infra_envs(cluster_id) | ||
| log.info("Found %d InfraEnvs for cluster %s", len(infra_envs), cluster_id) | ||
|
|
||
| if len(infra_envs) != 1: | ||
| log.warning( | ||
| "cluster %s has %d infra_envs, expected 1", cluster_id, len(infra_envs) | ||
| ) | ||
| return "ERROR: this cluster doesn't have exactly 1 infra env, cannot manage static network config" | ||
|
|
||
| return json.dumps(infra_envs[0].get("static_network_config", [])) |
There was a problem hiding this comment.
Bug: double-encoded JSON returned for static_network_config
When the API returns a JSON string, json.dumps wraps it again producing a JSON string literal, not an array. Return the string as-is; handle list/None robustly.
- return json.dumps(infra_envs[0].get("static_network_config", []))
+ value = infra_envs[0].get("static_network_config")
+ if value is None:
+ return "[]"
+ if isinstance(value, str):
+ # API returns JSON string; return as-is
+ return value
+ if isinstance(value, list):
+ # Already a list; serialize to JSON array
+ return json.dumps(value)
+ log.warning(
+ "Unexpected type for static_network_config: %s",
+ type(value).__name__,
+ )
+ return "[]"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| client = InventoryClient(get_access_token_func()) | |
| infra_envs = await client.list_infra_envs(cluster_id) | |
| log.info("Found %d InfraEnvs for cluster %s", len(infra_envs), cluster_id) | |
| if len(infra_envs) != 1: | |
| log.warning( | |
| "cluster %s has %d infra_envs, expected 1", cluster_id, len(infra_envs) | |
| ) | |
| return "ERROR: this cluster doesn't have exactly 1 infra env, cannot manage static network config" | |
| return json.dumps(infra_envs[0].get("static_network_config", [])) | |
| client = InventoryClient(get_access_token_func()) | |
| infra_envs = await client.list_infra_envs(cluster_id) | |
| log.info("Found %d InfraEnvs for cluster %s", len(infra_envs), cluster_id) | |
| if len(infra_envs) != 1: | |
| log.warning( | |
| "cluster %s has %d infra_envs, expected 1", cluster_id, len(infra_envs) | |
| ) | |
| return "ERROR: this cluster doesn't have exactly 1 infra env, cannot manage static network config" | |
| value = infra_envs[0].get("static_network_config") | |
| if value is None: | |
| return "[]" | |
| if isinstance(value, str): | |
| # API returns JSON string; return as-is | |
| return value | |
| if isinstance(value, list): | |
| # Already a list; serialize to JSON array | |
| return json.dumps(value) | |
| log.warning( | |
| "Unexpected type for static_network_config: %s", | |
| type(value).__name__, | |
| ) | |
| return "[]" |
🤖 Prompt for AI Agents
In assisted_service_mcp/src/tools/network_tools.py around lines 211-221, the
code always does json.dumps on static_network_config which double-encodes when
the API already returns a JSON string; change the return logic to: retrieve
static_network_config into a variable, if it's a str return it unchanged, if
it's None return "[]" (or json.dumps([])), otherwise json.dumps the list/dict
value; ensure the function consistently returns a JSON string and handles
list/None/str robustly.
| result = await client.get_openshift_versions(True) | ||
| log.info("Successfully retrieved OpenShift versions") | ||
| return json.dumps(result) | ||
| except Exception as e: | ||
| log.error("Failed to retrieve OpenShift versions: %s", str(e)) | ||
| raise |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Doc/behavior mismatch and potential JSON serialization issue
- Line 40: Passing True returns only latest versions, but the docstring says “complete list”. Use False by default or expose an only_latest parameter.
- Lines 41-42: Result may be a model (not JSON-serializable). Safely handle .to_dict() when present.
Apply:
- result = await client.get_openshift_versions(True)
- log.info("Successfully retrieved OpenShift versions")
- return json.dumps(result)
+ result = await client.get_openshift_versions(False)
+ log.info("Successfully retrieved OpenShift versions")
+ payload = result.to_dict() if hasattr(result, "to_dict") else result
+ return json.dumps(payload)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| result = await client.get_openshift_versions(True) | |
| log.info("Successfully retrieved OpenShift versions") | |
| return json.dumps(result) | |
| except Exception as e: | |
| log.error("Failed to retrieve OpenShift versions: %s", str(e)) | |
| raise | |
| result = await client.get_openshift_versions(False) | |
| log.info("Successfully retrieved OpenShift versions") | |
| payload = result.to_dict() if hasattr(result, "to_dict") else result | |
| return json.dumps(payload) | |
| except Exception as e: | |
| log.error("Failed to retrieve OpenShift versions: %s", str(e)) | |
| raise |
🤖 Prompt for AI Agents
In assisted_service_mcp/src/tools/version_tools.py around lines 40-45, the call
uses client.get_openshift_versions(True) which returns only latest versions
contrary to the docstring ("complete list") — change the call to False or add an
only_latest parameter to the function and pass it through; also ensure the
returned result is JSON-serializable by converting any model objects to plain
dicts (handle single model, lists, and dicts by calling .to_dict() when present
or mapping items recursively) before calling json.dumps, and keep the existing
logging and exception re-raising.
050b604 to
d4e88df
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (11)
assisted_service_mcp/src/service_client/assisted_service_api.py (1)
17-21: Reorder first-party imports before relative ones to satisfy lint.The linter (C0411) is still failing because the first-party imports need to be grouped ahead of the relative imports. Moving the
assisted_service_mcp.src.*imports above the.imports will clear the warning and unblock CI.Apply this diff:
-from .logger import log -from .exceptions import sanitize_exceptions -from .helpers import Helpers -from assisted_service_mcp.src.metrics.metrics import API_CALL_LATENCY -from assisted_service_mcp.src.settings import get_setting +from assisted_service_mcp.src.metrics.metrics import API_CALL_LATENCY +from assisted_service_mcp.src.settings import get_setting +from .logger import log +from .exceptions import sanitize_exceptions +from .helpers import Helpersassisted_service_mcp/src/settings.py (1)
3-6: Group the typing imports at the top to resolve the import-order lint failure.Combine the typing imports into a single line (
Any, ClassVar, Literal, Optional) so the standard-library imports sit together ahead of third-party imports, satisfying C0411/C0412.Apply this diff:
-from typing import Optional, ClassVar -from typing import Literal -from typing import Any +from typing import Any, ClassVar, Literal, Optionalassisted_service_mcp/utils/auth.py (1)
3-7: Move thetypingimports ahead of third-party imports to clear lint.PEP 8 requires standard-library imports (
typing) before third-party (requests). Reordering resolves the C0411 warning.Apply this diff:
-from typing import Any, Callable - -import requests +from typing import Any, Callable + +import requestsassisted_service_mcp/src/tools/network_tools.py (2)
23-23: Silence unused-argument warnings without changing contractRename the unused token arg to underscore to satisfy the linter.
-async def validate_nmstate_yaml( - get_access_token_func: Callable[[], str], +async def validate_nmstate_yaml( + _: Callable[[], str], # unused; kept for MCP wrapper contract @@ -async def generate_nmstate_yaml( - get_access_token_func: Callable[[], str], +async def generate_nmstate_yaml( + _: Callable[[], str], # unused; kept for MCP wrapper contractAlso applies to: 60-60
211-221: Fix double-encoded JSON and relax “exactly 1 infra env” requirementThe API returns static_network_config as a JSON string; json.dumps double-encodes it. Also, prefer the shared helper that selects the first valid InfraEnv.
Based on learnings
- client = InventoryClient(get_access_token_func()) - infra_envs = await client.list_infra_envs(cluster_id) - log.info("Found %d InfraEnvs for cluster %s", len(infra_envs), cluster_id) - - if len(infra_envs) != 1: - log.warning( - "cluster %s has %d infra_envs, expected 1", cluster_id, len(infra_envs) - ) - return "ERROR: this cluster doesn't have exactly 1 infra env, cannot manage static network config" - - return json.dumps(infra_envs[0].get("static_network_config", [])) + client = InventoryClient(get_access_token_func()) + infra_env_id = await _get_cluster_infra_env_id(client, cluster_id) + infra_env = await client.get_infra_env(infra_env_id) + value = getattr(infra_env, "static_network_config", None) + if value is None: + return "[]" + if isinstance(value, str): + # API responses provide a JSON string; return as-is + return value + if isinstance(value, list): + # Already parsed list; serialize to JSON array + return json.dumps(value) + log.warning( + "Unexpected type for static_network_config: %s", + type(value).__name__, + ) + return "[]"assisted_service_mcp/src/tools/download_tools.py (1)
52-55: Standardize return type to JSON across all pathsErrors/no-results return plain strings while success returns JSON. This breaks clients expecting JSON.
try: token = get_access_token_func() client = InventoryClient(token) infra_envs = await client.list_infra_envs(cluster_id) except Exception as e: log.error("Failed to retrieve infrastructure environments: %s", e) - return f"Error retrieving ISO URLs: {str(e)}" + return json.dumps({"error": f"Error retrieving ISO URLs: {str(e)}"}) if not infra_envs: log.info("No infrastructure environments found for cluster %s", cluster_id) - return "No ISO download URLs found for this cluster." + return json.dumps({"error": "No ISO download URLs found for this cluster."}) @@ if not iso_info: log.info( "No ISO download URLs found in infrastructure environments for cluster %s", cluster_id, ) - return "No ISO download URLs found for this cluster." + return json.dumps({"error": "No ISO download URLs found for this cluster."})Also applies to: 56-59, 87-93
tests/test_metrics.py (1)
2-3: Add FastAPI to test dependencies.The tests require
fastapibut it's not listed in the test dependencies, causingModuleNotFoundError. Addfastapito your test/dev requirements (e.g.,pyproject.tomlunder test dependencies orrequirements-dev.txt).assisted_service_mcp/src/tools/version_tools.py (1)
40-42: Address the documented doc/behavior mismatch and potential JSON serialization issue.The docstring states "complete list" but the implementation passes
Truetoget_openshift_versions(), which returns only latest versions. Additionally, the result may be a model object that requires.to_dict()conversion beforejson.dumps().This issue was previously flagged in past reviews and remains unaddressed.
assisted_service_mcp/src/service_client/logger.py (2)
87-90: Apply the previously suggested fixes to resolve E1101 and C0415.The pylint errors remain unaddressed. Apply the suggested fixes:
- from assisted_service_mcp.src.settings import settings + from assisted_service_mcp.src.settings import settings # pylint: disable=import-outside-toplevel - level = settings.LOGGING_LEVEL - return getattr(logging, level.upper(), logging.INFO) if level else logging.INFO + level = getattr(settings, "LOGGING_LEVEL", None) + if isinstance(level, str) and level: + return getattr(logging, level.upper(), logging.INFO) + return logging.INFO
144-150: Apply pylint disable comments for intentional import and global usage.The intentional import-inside-function and global rebinding still trigger lint warnings. Apply the previously suggested fixes:
- from assisted_service_mcp.src.settings import settings + from assisted_service_mcp.src.settings import settings # pylint: disable=import-outside-toplevel # Resolve logger name, falling back to a stable default logger_name = settings.LOGGER_NAME or "assisted-service-mcp" # Rebind the module logger if the configured name differs - global log + global log # pylint: disable=global-statementassisted_service_mcp/src/tools/cluster_tools.py (1)
415-416: Move the shared helper import to module level.This import-inside-function was previously flagged as unnecessary. No circular dependency has been observed between
cluster_toolsandshared_helpers.from assisted_service_mcp.src.service_client.assisted_service_api import InventoryClient from assisted_service_mcp.src.service_client.helpers import Helpers from assisted_service_mcp.src.service_client.logger import log +from assisted_service_mcp.src.tools.shared_helpers import _get_cluster_infra_env_id @@ - # Import helper function here to avoid circular imports - from assisted_service_mcp.src.tools.shared_helpers import _get_cluster_infra_env_id -
🧹 Nitpick comments (8)
assisted_service_mcp/src/tools/shared_helpers.py (1)
29-38: Pick the first InfraEnv that actually has an ID.If the first entry lacks an
idwhile later ones are valid, we raise even though usable data exists. Iterate until you find a truthy ID before giving up.Apply this diff:
- infra_env_id = infra_envs[0].get("id") - if not infra_env_id: - raise ValueError(f"No InfraEnv with valid ID found for cluster {cluster_id}") - - log.info("Using InfraEnv %s for cluster %s", infra_env_id, cluster_id) - return infra_env_id + for infra_env in infra_envs: + infra_env_id = infra_env.get("id") + if infra_env_id: + log.info("Using InfraEnv %s for cluster %s", infra_env_id, cluster_id) + return infra_env_id + + raise ValueError(f"No InfraEnv with valid ID found for cluster {cluster_id}")assisted_service_mcp/src/tools/event_tools.py (1)
51-53: Log stack traces for failures (use log.exception) and consider consistent error surfaceUse log.exception to capture stack traces. Consider aligning error handling (raise vs JSON error) with other tools for consistency.
- except Exception as e: - log.error("Failed to retrieve events for cluster %s: %s", cluster_id, str(e)) - raise + except Exception as e: + log.exception("Failed to retrieve events for cluster %s", cluster_id) + raise- except Exception as e: - log.error( - "Failed to retrieve events for host %s in cluster %s: %s", - host_id, - cluster_id, - str(e), - ) - raise + except Exception as e: + log.exception( + "Failed to retrieve events for host %s in cluster %s", + host_id, + cluster_id, + ) + raiseAlso applies to: 105-112
assisted_service_mcp/src/tools/download_tools.py (1)
107-112: Constrain file_name to valid choicesEnforce allowed values at type level to prevent invalid requests.
-from typing import Annotated, Callable +from typing import Annotated, Callable, Literal @@ - file_name: Annotated[ - str, + file_name: Annotated[ + Literal["kubeconfig", "kubeconfig-noingress", "kubeadmin-password"], Field( description="The type of credential file to download. Valid options: 'kubeconfig' (standard kubeconfig for cluster access - use this), 'kubeconfig-noingress' (kubeconfig without ingress), 'kubeadmin-password' (the kubeadmin user password)." ), ],assisted_service_mcp/src/utils/static_net/template.py (1)
124-124: Avoid passing None values into the template contextUse exclude_none to minimize conditionals and reduce chance of “None” leaking.
-return yaml.dump(yaml.safe_load(NMSTATE_TEMPLATE.render(**params.model_dump()))) +return yaml.dump( + yaml.safe_load(NMSTATE_TEMPLATE.render(**params.model_dump(exclude_none=True))) +)assisted_service_mcp/src/tools/network_tools.py (1)
51-55: Docstring/behavior mismatch: function raises on invalid YAMLThe function raises on invalid YAML, but docstring says it returns an error message. Update the docstring to reflect actual behavior.
- Returns: - str: "YAML is valid" if successful, otherwise error message. + Returns: + str: "YAML is valid" if successful. + + Raises: + ValueError: If the YAML is invalid.tests/test_logger_filter.py (1)
4-5: Consider exposing a public test helper for filtering.The tests access
SensitiveFormatter._filterdirectly (a protected member). While this is functional, consider adding a publicfilter_text()method toSensitiveFormatteror a test utility module to avoid the protected-access warning.Alternatively, if the protected access is intentional for testing internals, you can suppress the warning with:
def filter_text(text: str) -> str: return SensitiveFormatter._filter(text) # pylint: disable=protected-accesstests/test_api.py (1)
6-15: Consider test isolation for module reloads.The
import_api_with_transporthelper reloads modules to apply new settings. To ensure test isolation and prevent side effects, consider adding cleanup to restore the original module state after each test:@pytest.fixture def cleanup_modules(): """Restore modules after test.""" original_modules = sys.modules.copy() yield # Restore original modules sys.modules.clear() sys.modules.update(original_modules)Then use it in tests:
def test_api_uses_sse_when_configured(cleanup_modules) -> None: ...tests/test_settings.py (1)
6-6: Add return type hint.The function signature has
# type: ignore[no-untyped-def]but could benefit from an explicit return type:def reload_settings_with_env(env: dict[str, str]) -> Settings:This would improve type safety and remove the need for the ignore comment.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (43)
Dockerfile(2 hunks)Makefile(2 hunks)README.md(2 hunks)assisted_service_mcp/__init__.py(1 hunks)assisted_service_mcp/src/__init__.py(1 hunks)assisted_service_mcp/src/api.py(1 hunks)assisted_service_mcp/src/main.py(1 hunks)assisted_service_mcp/src/mcp.py(1 hunks)assisted_service_mcp/src/service_client/assisted_service_api.py(3 hunks)assisted_service_mcp/src/service_client/exceptions.py(1 hunks)assisted_service_mcp/src/service_client/helpers.py(1 hunks)assisted_service_mcp/src/service_client/logger.py(1 hunks)assisted_service_mcp/src/settings.py(1 hunks)assisted_service_mcp/src/tools/__init__.py(1 hunks)assisted_service_mcp/src/tools/cluster_tools.py(1 hunks)assisted_service_mcp/src/tools/download_tools.py(1 hunks)assisted_service_mcp/src/tools/event_tools.py(1 hunks)assisted_service_mcp/src/tools/host_tools.py(1 hunks)assisted_service_mcp/src/tools/network_tools.py(1 hunks)assisted_service_mcp/src/tools/shared_helpers.py(1 hunks)assisted_service_mcp/src/tools/version_tools.py(1 hunks)assisted_service_mcp/src/utils/static_net/config.py(2 hunks)assisted_service_mcp/src/utils/static_net/template.py(5 hunks)assisted_service_mcp/utils/__init__.py(1 hunks)assisted_service_mcp/utils/auth.py(1 hunks)assisted_service_mcp/utils/helpers.py(1 hunks)integration_test/performance/README.md(1 hunks)pyproject.toml(4 hunks)pyrightconfig.json(1 hunks)server.py(0 hunks)tests/test_api.py(1 hunks)tests/test_assisted_service_api.py(4 hunks)tests/test_auth.py(1 hunks)tests/test_helpers.py(1 hunks)tests/test_integration_api.py(1 hunks)tests/test_logger_filter.py(1 hunks)tests/test_mcp.py(1 hunks)tests/test_metrics.py(1 hunks)tests/test_server.py(0 hunks)tests/test_settings.py(1 hunks)tests/test_shared_helpers.py(1 hunks)tests/test_static_net.py(3 hunks)tests/test_tools_module.py(1 hunks)
💤 Files with no reviewable changes (2)
- server.py
- tests/test_server.py
✅ Files skipped from review due to trivial changes (2)
- assisted_service_mcp/src/tools/init.py
- assisted_service_mcp/src/init.py
🚧 Files skipped from review as they are similar to previous changes (15)
- README.md
- assisted_service_mcp/init.py
- assisted_service_mcp/src/api.py
- assisted_service_mcp/src/service_client/helpers.py
- assisted_service_mcp/src/main.py
- Makefile
- pyproject.toml
- tests/test_helpers.py
- tests/test_integration_api.py
- assisted_service_mcp/src/mcp.py
- integration_test/performance/README.md
- assisted_service_mcp/utils/helpers.py
- assisted_service_mcp/src/utils/static_net/config.py
- tests/test_auth.py
- tests/test_mcp.py
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: carbonin
PR: openshift-assisted/assisted-service-mcp#111
File: pyproject.toml:9-9
Timestamp: 2025-09-25T19:01:36.933Z
Learning: The `mcp` Python package (mcp>=1.15.0) includes FastMCP functionality and provides the same import path `from mcp.server.fastmcp import FastMCP` for backward compatibility with the standalone `fastmcp` package. This allows drop-in replacement when migrating from `fastmcp>=2.8.0` to `mcp>=1.15.0` without requiring code changes.
📚 Learning: 2025-09-09T18:51:46.598Z
Learnt from: keitwb
PR: openshift-assisted/assisted-service-mcp#91
File: service_client/static_net.py:21-36
Timestamp: 2025-09-09T18:51:46.598Z
Learning: In the assisted-service API, the static_network_config field is typed as a list when input to the API but comes back out as a string in responses. Functions processing this field from API responses should handle string inputs only.
Applied to files:
assisted_service_mcp/src/tools/network_tools.py
🧬 Code graph analysis (15)
assisted_service_mcp/utils/auth.py (2)
assisted_service_mcp/src/settings.py (1)
get_setting(184-193)tests/test_auth.py (1)
get_context(22-23)
tests/test_logger_filter.py (1)
assisted_service_mcp/src/service_client/logger.py (2)
SensitiveFormatter(15-76)_filter(28-63)
assisted_service_mcp/src/service_client/assisted_service_api.py (4)
assisted_service_mcp/src/service_client/exceptions.py (1)
sanitize_exceptions(24-58)assisted_service_mcp/src/service_client/helpers.py (1)
Helpers(7-38)assisted_service_mcp/src/metrics/metrics.py (1)
metrics(69-71)assisted_service_mcp/src/settings.py (1)
get_setting(184-193)
assisted_service_mcp/src/tools/host_tools.py (3)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (2)
InventoryClient(26-540)update_host(453-481)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
tests/test_metrics.py (1)
assisted_service_mcp/src/metrics/metrics.py (3)
metrics(69-71)initiate_metrics(44-48)track_tool_usage(51-65)
tests/test_shared_helpers.py (2)
assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)assisted_service_mcp/src/service_client/assisted_service_api.py (1)
list_infra_envs(218-236)
assisted_service_mcp/src/tools/shared_helpers.py (2)
assisted_service_mcp/src/service_client/assisted_service_api.py (2)
InventoryClient(26-540)list_infra_envs(218-236)tests/test_assisted_service_api.py (1)
client(32-37)
tests/test_assisted_service_api.py (2)
assisted_service_mcp/src/service_client/assisted_service_api.py (1)
InventoryClient(26-540)assisted_service_mcp/src/service_client/exceptions.py (1)
AssistedServiceAPIError(15-16)
assisted_service_mcp/src/tools/download_tools.py (3)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (3)
list_infra_envs(218-236)get_infra_env_download_url(516-540)get_presigned_for_cluster_credentials(484-513)assisted_service_mcp/utils/helpers.py (1)
format_presigned_url(11-36)
assisted_service_mcp/src/tools/version_tools.py (3)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (3)
get_openshift_versions(389-407)get_operator_bundles(410-420)add_operator_bundle_to_cluster(423-450)tests/test_tools_module.py (2)
to_str(444-445)to_str(488-489)
tests/test_static_net.py (2)
assisted_service_mcp/src/utils/static_net/config.py (3)
remove_static_host_config_by_index(23-38)add_or_replace_static_host_config_yaml(41-70)validate_and_parse_nmstate(96-108)assisted_service_mcp/src/utils/static_net/template.py (2)
generate_nmstate_from_template(121-124)NMStateTemplateParams(102-118)
tests/test_tools_module.py (9)
assisted_service_mcp/src/mcp.py (1)
AssistedServiceMCPServer(26-159)assisted_service_mcp/src/service_client/assisted_service_api.py (14)
update_cluster(330-368)create_cluster(239-278)install_cluster(371-386)get_events(156-197)list_infra_envs(218-236)get_infra_env_download_url(516-540)get_presigned_for_cluster_credentials(484-513)update_host(453-481)add_operator_bundle_to_cluster(423-450)get_operator_bundles(410-420)get_infra_env(200-215)get_openshift_versions(389-407)create_infra_env(281-303)update_infra_env(306-327)assisted_service_mcp/src/tools/cluster_tools.py (5)
set_cluster_platform(290-330)set_cluster_vips(232-286)create_cluster(104-228)install_cluster(334-372)set_cluster_ssh_key(376-439)assisted_service_mcp/src/tools/event_tools.py (2)
cluster_events(12-53)host_events(57-112)assisted_service_mcp/src/tools/download_tools.py (2)
cluster_iso_download_url(14-95)cluster_credentials_download_url(99-167)assisted_service_mcp/src/tools/host_tools.py (1)
set_host_role(13-65)assisted_service_mcp/src/tools/version_tools.py (3)
add_operator_bundle_to_cluster(86-141)list_operator_bundles(49-82)list_versions(13-45)assisted_service_mcp/src/tools/network_tools.py (4)
validate_nmstate_yaml(22-55)alter_static_network_config_nmstate_for_host(106-175)list_static_network_config(179-221)generate_nmstate_yaml(59-102)assisted_service_mcp/src/utils/static_net/template.py (2)
NMStateTemplateParams(102-118)EthernetInterfaceParams(37-44)
assisted_service_mcp/src/tools/cluster_tools.py (4)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (8)
InventoryClient(26-540)get_cluster(116-140)list_clusters(143-153)create_cluster(239-278)create_infra_env(281-303)update_cluster(330-368)install_cluster(371-386)update_infra_env(306-327)assisted_service_mcp/src/service_client/helpers.py (1)
Helpers(7-38)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
assisted_service_mcp/src/tools/event_tools.py (2)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (1)
get_events(156-197)
assisted_service_mcp/src/tools/network_tools.py (5)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (4)
InventoryClient(26-540)get_infra_env(200-215)update_infra_env(306-327)list_infra_envs(218-236)assisted_service_mcp/src/utils/static_net/template.py (2)
NMStateTemplateParams(102-118)generate_nmstate_from_template(121-124)assisted_service_mcp/src/utils/static_net/config.py (3)
add_or_replace_static_host_config_yaml(41-70)remove_static_host_config_by_index(23-38)validate_and_parse_nmstate(96-108)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
🪛 GitHub Actions: Pyright
tests/test_metrics.py
[error] 2-2: Import 'fastapi' could not be resolved (reportMissingImports)
[error] 3-3: Import 'fastapi.testclient' could not be resolved (reportMissingImports)
🪛 GitHub Actions: Python linter
tests/test_logger_filter.py
[warning] 5-5: W0212: Access to a protected member _filter of a client class (protected-access)
[warning] 1-1: R0801: Similar lines in 2 files
assisted_service_mcp/src/service_client/assisted_service_api.py
[warning] 20-20: C0411: first party import "assisted_service_mcp.src.metrics.metrics.API_CALL_LATENCY" should be placed before local imports "logger.log", "exceptions.sanitize_exceptions", "helpers.Helpers" (wrong-import-order)
[warning] 21-21: C0411: first party import "assisted_service_mcp.src.settings.get_setting" should be placed before local imports "logger.log", "exceptions.sanitize_exceptions", "helpers.Helpers" (wrong-import-order)
tests/test_metrics.py
[error] 2-2: E0401: Unable to import 'fastapi' (import-error)
[error] 3-3: E0401: Unable to import 'fastapi.testclient' (import-error)
assisted_service_mcp/src/settings.py
[warning] 147-147: Redefining name 'settings' from outer scope (redefined-outer-name)
assisted_service_mcp/src/service_client/logger.py
[error] 90-90: E1101: Instance of 'FieldInfo' has no 'upper' member (no-member)
[warning] 150-150: W0603: Using the global statement (global-statement)
assisted_service_mcp/src/tools/network_tools.py
[warning] 23-23: W0613: Unused argument 'get_access_token_func' (unused-argument)
[warning] 60-60: W0613: Unused argument 'get_access_token_func' (unused-argument)
🪛 GitHub Actions: Unit Tests
tests/test_metrics.py
[error] 1-1: ModuleNotFoundError: No module named 'fastapi' during test collection. Ensure 'fastapi' is installed or listed in dependencies. Command: uv run --group test pytest --cov=service_client --cov=server --cov-report term-missing
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Red Hat Konflux / assisted-service-mcp-saas-main-on-pull-request
🔇 Additional comments (16)
assisted_service_mcp/src/service_client/exceptions.py (1)
12-12: LGTM: Import path updated correctly.The change from absolute to relative import aligns with the new module structure.
tests/test_shared_helpers.py (1)
8-28: LGTM: Comprehensive test coverage for the helper.The tests cover the key scenarios:
- Successful InfraEnv ID retrieval
- No InfraEnvs found (ValueError)
- Missing ID field (ValueError)
The use of
AsyncMockis appropriate for async testing.assisted_service_mcp/src/tools/host_tools.py (1)
12-65: LGTM: Well-structured host role assignment tool.The implementation is clean with:
- Comprehensive type hints using
AnnotatedandField- Detailed docstring with examples and prerequisites
- Proper use of the shared helper
_get_cluster_infra_env_id- Appropriate telemetry via
@track_tool_usage()The error handling is delegated to the underlying client and helper, which raise appropriate exceptions.
tests/test_assisted_service_api.py (3)
12-13: LGTM: Import paths updated correctly.The imports now use the new module structure under
assisted_service_mcp.src.service_client.
61-70: LGTM: Settings patch approach is cleaner.The change from patching environment variables to directly patching
settings.settingsattributes is more direct and aligns with the centralized configuration approach. This provides better test isolation and clarity.
141-141: LGTM: ApiClient patch target updated correctly.The patch target now reflects the new module structure at
assisted_service_mcp.src.service_client.assisted_service_api.tests/test_settings.py (1)
19-53: LGTM: Comprehensive settings validation tests.The tests cover:
- Default values verification
- Environment variable overrides
- Validation for invalid transport values
The reload pattern ensures settings are properly applied during testing.
tests/test_tools_module.py (4)
8-104: LGTM! Test structure and mocking patterns are consistent.The test functions follow a clear pattern: instantiate the MCP server, patch the InventoryClient and auth token, invoke the tool function, and assert the expected response. The use of
AsyncMockfor async methods and the mock object construction withtype()forto_str()methods is appropriate.
106-127: LGTM!The test correctly validates that the
cluster_eventstool returns parseable JSON and contains the expected event data.
129-304: LGTM! Tests cover presigned URLs and helper function patching correctly.The tests properly handle presigned URL responses with optional expiration times and correctly patch shared helper functions like
_get_cluster_infra_env_id.
306-514: LGTM! Comprehensive test coverage with proper error handling.The remaining tests provide excellent coverage of network configuration tools, version management, cluster creation, and partial failure scenarios. The use of
pytest.raiseswithmatchfor error validation and the testing of partial failure messages demonstrates thorough test design.assisted_service_mcp/src/tools/version_tools.py (2)
48-83: LGTM!The
list_operator_bundlesfunction correctly handles the response. TheInventoryClient.get_operator_bundles()method already converts model objects to dictionaries, sojson.dumps()will work properly.
127-133: Confirm models.Cluster implementsto_str()
Ensuremodels.Clusterdefines or inherits ato_str()method before calling.to_str()in assisted_service_mcp/src/tools/version_tools.py:127-133 to prevent an AttributeError at runtime.assisted_service_mcp/src/service_client/logger.py (2)
15-77: LGTM! Comprehensive sensitive data redaction.The
SensitiveFormattercorrectly handles both dictionary-style and object-style representations of sensitive fields, preserving the original formatting while redacting values.
159-177: LGTM! Handler management prevents duplication.The handler reset logic correctly prevents duplicate handlers on reconfiguration, and the use of
NullHandler()for urllib3 is appropriate.assisted_service_mcp/src/tools/cluster_tools.py (1)
13-439: LGTM! Well-structured cluster management tools with comprehensive documentation.All cluster tool functions follow consistent patterns with proper error handling, logging, and comprehensive docstrings that include examples, prerequisites, and related tools. The validation logic in
create_clusterfor single-node platform combinations and the partial failure handling inset_cluster_ssh_keydemonstrate thoughtful implementation.
| "integration_test/performance" | ||
| ], | ||
| "excludeTests": false, | ||
| "useLibraryCodeForTypes": false |
There was a problem hiding this comment.
Re-enable useLibraryCodeForTypes to keep third-party typing coverage.
Setting this flag to false stops Pyright from inferring types from runtime library code when stubs are missing. Many of our dependencies (e.g., FastAPI, requests, prometheus-client) don’t ship complete stubs; with this setting, Pyright will treat their APIs as Unknown, masking regressions in our integrations. Please drop the entry or set it back to true so library usage remains type-checked.
- "useLibraryCodeForTypes": false
+ "useLibraryCodeForTypes": true📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "useLibraryCodeForTypes": false | |
| "useLibraryCodeForTypes": true |
🤖 Prompt for AI Agents
In pyrightconfig.json at line 12, the "useLibraryCodeForTypes" entry is set to
false; change it back to true (or remove the property entirely) so Pyright will
use library code for type inference—update the JSON accordingly and ensure the
file remains valid JSON after the change.
carbonin
left a comment
There was a problem hiding this comment.
Just a first pass as you're probably still working on this.
Also what is the upside of wrapping the tool functions and injecting the auth info?
Seems like a big change for something that was generally working fine before.
|
|
||
| Examples: | ||
| - cluster_info("550e8400-e29b-41d4-a716-446655440000") | ||
| - After creating a cluster, use this to verify the configuration |
There was a problem hiding this comment.
Why is this in examples?
Also why is Examples necessary?
There was a problem hiding this comment.
Including examples initially seemed like a good idea, but I agree that in most cases they ended up feeling a bit redundant or forced. I was planning to revisit them and keep only the ones that truly add value, just haven’t had the time yet.
|
|
||
| Prerequisites: | ||
| - Valid cluster UUID (from list_clusters or create_cluster) | ||
| - OCM offline token for authentication |
There was a problem hiding this comment.
Won't this be required everywhere? Also since the model isn't really aware of auth I'm not sure this is needed.
| Prerequisites: | ||
| - Valid OCM offline token for authentication | ||
| - OpenShift version from list_versions | ||
| - Configured DNS domain |
There was a problem hiding this comment.
Do we mention this elsewhere?
It's not really a prereq for calling this tool, right?
|
|
||
| Sets the API and ingress VIPs required for HA clusters on baremetal, vsphere, and nutanix | ||
| platforms. VIPs are NOT needed for single-node clusters or clusters on 'none' or 'oci' | ||
| platforms. The IP addresses must be within the cluster's network subnet, not assigned to |
There was a problem hiding this comment.
Maybe we should specify machine network subnet here?
Seems like this could be mistaken for the internal cluster network.
|
|
||
|
|
||
| @track_tool_usage() | ||
| async def list_operator_bundles(get_access_token_func: Callable[[], str]) -> str: |
There was a problem hiding this comment.
I wouldn't say the operator versions tools are really related to "versions"
In the monolithic server.py getting the token was straightforward as every tool could see mcp and its request context. In the new modular layout, tools are plain functions in separate modules and don’t naturally have the request, so in some way or another I had to pass extra data to the tools, while keeping their signatures clean. There were a few feasible options, this one seemed just fine enough to me and easy to test. |
d4e88df to
fb1108a
Compare
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (8)
assisted_service_mcp/src/logger.py (2)
143-178: Silence intentional import-outside-toplevel and global-statement warnings.The past review comment correctly identified that the intentional patterns here trigger lint warnings that should be silenced with inline disable comments.
Apply this diff:
- from assisted_service_mcp.src.settings import settings + from assisted_service_mcp.src.settings import settings # pylint: disable=import-outside-toplevel # Resolve logger name, falling back to a stable default logger_name = settings.LOGGER_NAME or "assisted-service-mcp" # Rebind the module logger if the configured name differs - global log + global log # pylint: disable=global-statement
79-91: Fix pylint E1101 and make LOGGING_LEVEL handling robust.The past review comment correctly identified that
settings.LOGGING_LEVELmight be a FieldInfo object, and calling.upper()on it will fail. The function also needs to silence the intentional C0415 (import-outside-toplevel) warning.Apply this diff:
def get_logging_level() -> int: """ Get the logging level from settings. Returns: int: The logging level (defaults to INFO if not set or invalid). """ # Import here to avoid circular dependency at module load time - from assisted_service_mcp.src.settings import settings + from assisted_service_mcp.src.settings import settings # pylint: disable=import-outside-toplevel - level = settings.LOGGING_LEVEL - return getattr(logging, str(level).upper(), logging.INFO) if level else logging.INFO + level = getattr(settings, "LOGGING_LEVEL", None) + if isinstance(level, str) and level: + return getattr(logging, level.upper(), logging.INFO) + return logging.INFOtests/test_metrics.py (1)
1-45: Missing FastAPI dependency causes test failures.The test imports
fastapiandfastapi.testclientbut the package is not declared in test or dev dependencies, causingModuleNotFoundErrorin the pipeline.Add
fastapito your test dependencies inpyproject.toml:test = [ "pytest>=8.0.0", "pytest-asyncio>=0.23.0", "pytest-mock>=3.12.0", "pytest-cov>=4.0.0", + "fastapi>=0.100.0", ]assisted_service_mcp/src/service_client/assisted_service_api.py (1)
17-21: Fix import order to resolve linter warning.The linter flags that first-party imports (
assisted_service_mcp.src.logger,assisted_service_mcp.src.metrics.metrics, andassisted_service_mcp.src.settings) should be placed before local relative imports (.logger,.exceptions,.helpers).Apply this diff to fix the import order:
+from assisted_service_mcp.src.logger import log +from assisted_service_mcp.src.metrics.metrics import API_CALL_LATENCY +from assisted_service_mcp.src.settings import get_setting + -from assisted_service_mcp.src.logger import log from .exceptions import sanitize_exceptions from .helpers import Helpers -from assisted_service_mcp.src.metrics.metrics import API_CALL_LATENCY -from assisted_service_mcp.src.settings import get_settingassisted_service_mcp/src/settings.py (1)
147-177: Avoid shadowing the module-levelsettingsinstance.The
validate_configparameter name conflicts with the globalsettings, triggeringredefined-outer-name. Rename the parameter (e.g.,config) and update references so lint passes and the intent stays clear.Apply this diff:
-def validate_config(settings: Settings) -> None: +def validate_config(config: Settings) -> None: """Validate configuration settings. Performs validation to ensure required settings are present and values are within acceptable ranges. Args: - settings: Settings instance to validate. + config: Settings instance to validate. Raises: ValueError: If required configuration is missing or invalid. """ # Validate port range - if not 1024 <= settings.MCP_PORT <= 65535: + if not 1024 <= config.MCP_PORT <= 65535: raise ValueError( - f"MCP_PORT must be between 1024 and 65535, got {settings.MCP_PORT}" + f"MCP_PORT must be between 1024 and 65535, got {config.MCP_PORT}" ) # Validate log level valid_log_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] - if settings.LOGGING_LEVEL.upper() not in valid_log_levels: + if config.LOGGING_LEVEL.upper() not in valid_log_levels: raise ValueError( - f"LOGGING_LEVEL must be one of {valid_log_levels}, got {settings.LOGGING_LEVEL}" + f"LOGGING_LEVEL must be one of {valid_log_levels}, got {config.LOGGING_LEVEL}" ) # Validate transport protocol valid_transports = ["sse", "streamable-http"] - if settings.TRANSPORT not in valid_transports: + if config.TRANSPORT not in valid_transports: raise ValueError( - f"TRANSPORT must be one of {valid_transports}, got {settings.TRANSPORT}" + f"TRANSPORT must be one of {valid_transports}, got {config.TRANSPORT}" )assisted_service_mcp/src/tools/network_tools.py (3)
21-30: Silence unused-argument warnings by renaming the parameter.This tool doesn't need the token; rename
get_access_token_functo_to signal intentional unusedness while preserving the wrapper contract.This issue was already identified in a previous review.
58-67: Silence unused-argument warnings by renaming the parameter.This tool doesn't need the token; rename
get_access_token_functo_to signal intentional unusedness while preserving the wrapper contract.This issue was already identified in a previous review.
211-221: Bug: double-encoded JSON returned for static_network_config.When the API returns a JSON string,
json.dumpswraps it again producing a JSON string literal, not an array. The retrieved learning confirms thatstatic_network_configis a list on input but a string in API responses.This issue was already identified in a previous review. Based on learnings.
🧹 Nitpick comments (3)
assisted_service_mcp/src/logger.py (1)
1-179: Consider the logger placement as project evolves.@carbonin raised a valid point: this logger module provides project-wide functionality and is now imported from
assisted_service_mcp.src.loggerby the service_client package. While the current structure works, as the codebase grows you might consider whether a top-levelassisted_service_mcp.loggerorassisted_service_mcp.common.loggerwould better reflect its cross-cutting nature.The current placement is acceptable for now since the entire codebase is under active refactoring.
tests/test_api.py (1)
6-15: Consider adding return type hint for clarity.The helper function works correctly but would benefit from a return type annotation.
Apply this diff to add the return type hint:
-def import_api_with_transport(transport: str) -> object: +def import_api_with_transport(transport: str) -> Any:Also add the import at the top:
import importlib import sys +from typing import Any + import pytesttests/test_settings.py (1)
6-16: Add return type hint for better type safety.The helper function would benefit from a return type annotation to clarify what it returns.
Apply this diff:
-def reload_settings_with_env(env: dict[str, str]): # type: ignore[no-untyped-def] +def reload_settings_with_env(env: dict[str, str]) -> Any:Add the import at the top:
import importlib import sys +from typing import Any + import pytest
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (45)
Dockerfile(2 hunks)Makefile(2 hunks)README.md(2 hunks)assisted_service_mcp/__init__.py(1 hunks)assisted_service_mcp/src/__init__.py(1 hunks)assisted_service_mcp/src/api.py(1 hunks)assisted_service_mcp/src/logger.py(1 hunks)assisted_service_mcp/src/main.py(1 hunks)assisted_service_mcp/src/mcp.py(1 hunks)assisted_service_mcp/src/service_client/__init__.py(1 hunks)assisted_service_mcp/src/service_client/assisted_service_api.py(3 hunks)assisted_service_mcp/src/service_client/exceptions.py(1 hunks)assisted_service_mcp/src/service_client/helpers.py(1 hunks)assisted_service_mcp/src/settings.py(1 hunks)assisted_service_mcp/src/tools/__init__.py(1 hunks)assisted_service_mcp/src/tools/cluster_tools.py(1 hunks)assisted_service_mcp/src/tools/download_tools.py(1 hunks)assisted_service_mcp/src/tools/event_tools.py(1 hunks)assisted_service_mcp/src/tools/host_tools.py(1 hunks)assisted_service_mcp/src/tools/network_tools.py(1 hunks)assisted_service_mcp/src/tools/shared_helpers.py(1 hunks)assisted_service_mcp/src/tools/version_tools.py(1 hunks)assisted_service_mcp/src/utils/static_net/config.py(2 hunks)assisted_service_mcp/src/utils/static_net/template.py(5 hunks)assisted_service_mcp/utils/__init__.py(1 hunks)assisted_service_mcp/utils/auth.py(1 hunks)assisted_service_mcp/utils/helpers.py(1 hunks)integration_test/performance/README.md(1 hunks)pyproject.toml(4 hunks)pyrightconfig.json(1 hunks)server.py(0 hunks)service_client/logger.py(0 hunks)tests/test_api.py(1 hunks)tests/test_assisted_service_api.py(4 hunks)tests/test_auth.py(1 hunks)tests/test_helpers.py(1 hunks)tests/test_integration_api.py(1 hunks)tests/test_logger_filter.py(1 hunks)tests/test_mcp.py(1 hunks)tests/test_metrics.py(1 hunks)tests/test_server.py(0 hunks)tests/test_settings.py(1 hunks)tests/test_shared_helpers.py(1 hunks)tests/test_static_net.py(3 hunks)tests/test_tools_module.py(1 hunks)
💤 Files with no reviewable changes (3)
- server.py
- tests/test_server.py
- service_client/logger.py
✅ Files skipped from review due to trivial changes (2)
- assisted_service_mcp/src/init.py
- assisted_service_mcp/utils/init.py
🚧 Files skipped from review as they are similar to previous changes (22)
- assisted_service_mcp/src/service_client/helpers.py
- assisted_service_mcp/src/tools/init.py
- tests/test_logger_filter.py
- tests/test_assisted_service_api.py
- assisted_service_mcp/init.py
- assisted_service_mcp/src/tools/host_tools.py
- README.md
- assisted_service_mcp/src/utils/static_net/config.py
- assisted_service_mcp/utils/helpers.py
- tests/test_integration_api.py
- assisted_service_mcp/src/utils/static_net/template.py
- tests/test_static_net.py
- assisted_service_mcp/src/service_client/exceptions.py
- assisted_service_mcp/src/tools/event_tools.py
- tests/test_mcp.py
- assisted_service_mcp/src/tools/version_tools.py
- integration_test/performance/README.md
- tests/test_tools_module.py
- assisted_service_mcp/src/tools/download_tools.py
- assisted_service_mcp/utils/auth.py
- assisted_service_mcp/src/api.py
- pyrightconfig.json
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-09T18:51:46.598Z
Learnt from: keitwb
PR: openshift-assisted/assisted-service-mcp#91
File: service_client/static_net.py:21-36
Timestamp: 2025-09-09T18:51:46.598Z
Learning: In the assisted-service API, the static_network_config field is typed as a list when input to the API but comes back out as a string in responses. Functions processing this field from API responses should handle string inputs only.
Applied to files:
assisted_service_mcp/src/tools/network_tools.py
🧬 Code graph analysis (10)
tests/test_metrics.py (1)
assisted_service_mcp/src/metrics/metrics.py (3)
metrics(69-71)initiate_metrics(44-48)track_tool_usage(51-65)
assisted_service_mcp/src/main.py (2)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)initiate_metrics(44-48)assisted_service_mcp/src/mcp.py (1)
list_tools_sync(151-163)
assisted_service_mcp/src/tools/shared_helpers.py (2)
assisted_service_mcp/src/service_client/assisted_service_api.py (2)
InventoryClient(26-540)list_infra_envs(218-236)tests/test_assisted_service_api.py (1)
client(32-37)
tests/test_helpers.py (1)
assisted_service_mcp/src/service_client/helpers.py (1)
Helpers(7-38)
assisted_service_mcp/src/tools/network_tools.py (5)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (4)
InventoryClient(26-540)get_infra_env(200-215)update_infra_env(306-327)list_infra_envs(218-236)assisted_service_mcp/src/utils/static_net/template.py (2)
NMStateTemplateParams(102-118)generate_nmstate_from_template(121-124)assisted_service_mcp/src/utils/static_net/config.py (3)
add_or_replace_static_host_config_yaml(41-70)remove_static_host_config_by_index(23-38)validate_and_parse_nmstate(96-108)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
assisted_service_mcp/src/service_client/assisted_service_api.py (3)
assisted_service_mcp/src/service_client/exceptions.py (1)
sanitize_exceptions(24-58)assisted_service_mcp/src/service_client/helpers.py (1)
Helpers(7-38)assisted_service_mcp/src/settings.py (1)
get_setting(184-193)
tests/test_shared_helpers.py (2)
assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)assisted_service_mcp/src/service_client/assisted_service_api.py (1)
list_infra_envs(218-236)
assisted_service_mcp/src/mcp.py (2)
assisted_service_mcp/utils/auth.py (2)
get_offline_token(10-45)get_access_token(48-114)assisted_service_mcp/src/tools/cluster_tools.py (1)
cluster_info(14-55)
tests/test_auth.py (1)
assisted_service_mcp/utils/auth.py (2)
get_offline_token(10-45)get_access_token(48-114)
assisted_service_mcp/src/tools/cluster_tools.py (4)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (8)
InventoryClient(26-540)get_cluster(116-140)list_clusters(143-153)create_cluster(239-278)create_infra_env(281-303)update_cluster(330-368)install_cluster(371-386)update_infra_env(306-327)assisted_service_mcp/src/service_client/helpers.py (1)
Helpers(7-38)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
🪛 GitHub Actions: Pyright
tests/test_metrics.py
[error] 2-2: Pyright: Import "fastapi" could not be resolved (reportMissingImports)
[error] 3-3: Pyright: Import "fastapi.testclient" could not be resolved (reportMissingImports)
🪛 GitHub Actions: Python linter
tests/test_metrics.py
[error] 2-2: E0401: Unable to import 'fastapi' (import-error).
[error] 3-3: E0401: Unable to import 'fastapi.testclient' (import-error).
assisted_service_mcp/src/logger.py
[warning] 150-150: Using the global statement (global-statement).
assisted_service_mcp/src/tools/network_tools.py
[warning] 23-23: W0613: Unused argument 'get_access_token_func' (unused-argument).
[warning] 60-60: W0613: Unused argument 'get_access_token_func' (unused-argument).
assisted_service_mcp/src/service_client/assisted_service_api.py
[warning] 20-21: C0411: first party import 'assisted_service_mcp.src.metrics.metrics.API_CALL_LATENCY' should be placed before local imports 'exceptions.sanitize_exceptions', 'helpers.Helpers' (wrong-import-order).
[warning] 21-21: C0411: first party import 'assisted_service_mcp.src.settings.get_setting' should be placed before local imports 'exceptions.sanitize_exceptions', 'helpers.Helpers' (wrong-import-order).
assisted_service_mcp/src/settings.py
[warning] 147-147: Redefining name 'settings' from outer scope (redefined-outer-name).
🪛 GitHub Actions: Unit Tests
tests/test_metrics.py
[error] 1-1: ModuleNotFoundError: No module named 'fastapi' during test collection. Ensure 'fastapi' is installed and available in the test environment.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Red Hat Konflux / assisted-service-mcp-saas-main-on-pull-request
🔇 Additional comments (28)
pyproject.toml (2)
17-18: LGTM! Dependencies support the new settings architecture.The addition of
pydantic-settings>=2.6.0andpython-dotenv>=1.0.0correctly supports the centralized settings module introduced in this PR. These dependencies align with the pydantic v2 ecosystem and enable environment-based configuration.Based on learnings: pydantic-settings 2.6.0+ includes proper validation and dotenv integration support. Ensure pydantic-core versions are compatible if you encounter schema validation issues.
62-62: LGTM! Appropriate linting exception for circular import avoidance.Disabling
import-outside-toplevelis justified given the intentional use of function-scoped imports inlogger.pyandsettings.pyto avoid circular dependencies at module load time. This is a standard pattern for resolving circular imports in Python.tests/test_helpers.py (1)
6-6: LGTM! Import path aligns with package refactor.The import path update from
service_client.helperstoassisted_service_mcp.src.service_client.helperscorrectly reflects the new package structure introduced in this PR.assisted_service_mcp/src/service_client/__init__.py (1)
8-8: LGTM! Logger import reflects centralized logging architecture.The import change from relative (
.logger) to absolute (assisted_service_mcp.src.logger) correctly reflects that the logger module has been promoted to a project-wide utility rather than a service_client-specific module. This aligns with the architectural feedback from @carbonin and better represents the logger's cross-cutting nature.Makefile (2)
18-18: LGTM! Entry point aligns with package refactor.The change from
uv run server.pytouv run python -m assisted_service_mcp.src.maincorrectly invokes the new package-based entry point introduced in this PR.
33-33: LGTM! Coverage targets updated for new package structure.Adding
--cov=assisted_service_mcpensures the new package modules are included in coverage reports alongside the existingservice_clientcoverage. This provides comprehensive test coverage visibility.tests/test_shared_helpers.py (1)
1-28: LGTM! Comprehensive test coverage for shared helper.The tests provide good coverage of
_get_cluster_infra_env_id:
- Success case with valid InfraEnv ID
- Error case when no InfraEnvs exist
- Error case when InfraEnv lacks an ID
The use of
AsyncMockis appropriate and the test structure follows pytest best practices.Dockerfile (2)
14-14: LGTM! Simplified COPY aligns with package structure.The change from copying multiple individual files/directories to copying the entire
assisted_service_mcp/directory simplifies the Dockerfile and aligns with the new package-based structure. This reduces maintenance burden when adding new modules under the package.
25-25: LGTM! Container entrypoint uses new package-based entry point.The CMD update to use
python -m assisted_service_mcp.src.maincorrectly invokes the new package-based entry point, consistent with the Makefile and documentation updates across the PR.assisted_service_mcp/src/tools/shared_helpers.py (1)
7-41: LGTM! Well-structured helper with appropriate error handling.The
_get_cluster_infra_env_idhelper provides clear error messages for edge cases (no InfraEnvs, multiple InfraEnvs, missing ID) and includes informative logging. The implementation correctly handles the return type fromlist_infra_envsand gracefully degrades when multiple InfraEnvs exist by using the first valid one with a warning.tests/test_api.py (1)
18-27: LGTM! Tests appropriately verify transport-based configuration.Both test cases correctly verify that the api module exposes the required
appandserverattributes for each supported transport type.tests/test_auth.py (1)
1-66: LGTM! Comprehensive authentication test coverage.The test suite properly covers all authentication flows including:
- Environment variable vs. header precedence
- Missing token scenarios
- Authorization header extraction
- HTTP-based token exchange with proper mocking
The helper classes (
_ReqCtxand_MCP) effectively simulate the request context without requiring full server setup.assisted_service_mcp/src/main.py (1)
10-42: LGTM! Clean entry point with proper initialization and error handling.The main function appropriately:
- Logs configuration details for debugging
- Initializes metrics before server start
- Registers the metrics endpoint
- Handles both keyboard interrupt and unexpected exceptions
- Ensures cleanup via finally block
The call to
server.list_tools_sync()at line 25 is safe here since it occurs beforeuvicorn.run()starts the event loop.assisted_service_mcp/src/service_client/assisted_service_api.py (1)
42-44: LGTM! Constructor properly uses centralized settings.The refactor to read configuration at construction time via
get_setting()aligns with the new settings-driven architecture and enables better testability by allowing tests to patch settings before instantiation.tests/test_settings.py (1)
19-53: LGTM! Comprehensive settings test coverage.The test suite effectively validates:
- Default values for all configuration fields
- Environment variable overrides
- Validation for invalid transport values
The tests properly use module reloading to ensure environment changes take effect.
assisted_service_mcp/src/settings.py (1)
24-144: LGTM! Well-structured settings with Pydantic validation.The Settings class provides:
- Clear field definitions with descriptions and examples
- Type-safe configuration via Pydantic
- Environment variable loading via dotenv
- Comprehensive field metadata for documentation
The use of
BaseSettingsandFieldwithjson_schema_extramakes the configuration self-documenting and easy to validate.assisted_service_mcp/src/mcp.py (3)
33-54: LGTM! Robust initialization with proper error handling.The constructor appropriately:
- Configures transport based on settings
- Binds auth helpers to the MCP instance
- Registers all tools via
_register_mcp_tools()- Logs initialization status and catches/logs any exceptions
116-145: LGTM! Clever wrapper implementation for dependency injection.The
_wrap_toolmethod elegantly solves the token injection problem by:
- Wrapping the tool function to inject
_get_access_tokenas the first parameter- Removing the injected parameter from the exposed signature via
inspect.signature- Preserving function metadata with
@wrapsThis approach keeps tool function signatures clean while ensuring they receive the required auth token provider.
151-163: LGTM! Safe synchronous wrapper with event loop detection.The
list_tools_syncmethod properly handles the edge case where it might be called from an async context by:
- Detecting if an event loop is already running
- Using
asyncio.run()only when safe (no running loop)- Raising a clear error message if called from an async context
This prevents the common pitfall of nesting
asyncio.run()calls.assisted_service_mcp/src/tools/network_tools.py (2)
92-102: Exception handling is acceptable.The broad
Exceptioncatch at line 100 ensures the function doesn't raise unexpected errors to callers, which is appropriate for a tool function. The error messages are clear and logged.
105-175: LGTM!The function correctly uses
get_access_token_functo create the client (line 151), handles both add/replace and removal operations with appropriate validations, and returns the updated infrastructure environment.assisted_service_mcp/src/tools/cluster_tools.py (7)
13-55: LGTM!The function correctly retrieves and returns cluster information. The structure and error handling are appropriate.
58-100: LGTM!The function correctly lists clusters and formats the response as a JSON array with essential fields.
103-228: LGTM with minor observation.The cluster creation logic correctly handles platform selection, validates single-node constraints, and creates both the cluster and infrastructure environment.
Note: Line 199 hardcodes
"tags": "chatbot"for all clusters, which may be intentional for tracking MCP-created clusters.
231-286: LGTM!The function correctly configures VIPs for the cluster. The documentation and logic are clear.
289-330: LGTM!The function correctly updates the cluster platform with appropriate logging.
333-372: LGTM!The function correctly initiates cluster installation with appropriate logging and documentation.
375-439: LGTM!The function correctly updates both cluster and InfraEnv with the SSH key, handles partial failures gracefully with informative error messages, and returns appropriate status.
The import inside the function (lines 415-416) was previously reviewed and marked as addressed, indicating the team chose to keep it to avoid circular imports.
fb1108a to
3a2c8d9
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (5)
assisted_service_mcp/src/logger.py (2)
79-91: Address pylint warnings and make LOGGING_LEVEL handling more defensive.A previous review comment already identified issues here. The import inside the function should be silenced with
# pylint: disable=import-outside-toplevel, and the level access should usegetattr(settings, "LOGGING_LEVEL", None)withisinstancechecks to guard against attribute errors.As suggested in the previous review, apply defensive attribute access:
def get_logging_level() -> int: """ Get the logging level from settings. Returns: int: The logging level (defaults to INFO if not set or invalid). """ # Import here to avoid circular dependency at module load time - from assisted_service_mcp.src.settings import settings + from assisted_service_mcp.src.settings import settings # pylint: disable=import-outside-toplevel - level = settings.LOGGING_LEVEL - return getattr(logging, str(level).upper(), logging.INFO) if level else logging.INFO + level = getattr(settings, "LOGGING_LEVEL", None) + if isinstance(level, str) and level: + return getattr(logging, level.upper(), logging.INFO) + return logging.INFO
143-150: Silence intentional pylint warnings for import and global statement.A previous review comment already identified that the import inside the function (C0415) and global statement (W0603) need to be silenced with pylint disable comments.
Apply the suggested fix:
# Import inside function to avoid circular dependency - from assisted_service_mcp.src.settings import settings + from assisted_service_mcp.src.settings import settings # pylint: disable=import-outside-toplevel # Resolve logger name, falling back to a stable default logger_name = settings.LOGGER_NAME or "assisted-service-mcp" # Rebind the module logger if the configured name differs - global log + global log # pylint: disable=global-statementassisted_service_mcp/src/tools/download_tools.py (1)
47-95: Return JSON for every path.The error and no-results branches still return plain strings while the success path returns
json.dumps(...). Callers now have to guess the response format, which breaks JSON parsing. Please return JSON from every branch (e.g.,{"error": ...}for failures,{"message": ...}when nothing is found).except Exception as e: log.error("Failed to retrieve infrastructure environments: %s", e) - return f"Error retrieving ISO URLs: {str(e)}" + return json.dumps({"error": f"Error retrieving ISO URLs: {str(e)}"}) if not infra_envs: log.info("No infrastructure environments found for cluster %s", cluster_id) - return "No ISO download URLs found for this cluster." + return json.dumps({"error": "No ISO download URLs found for this cluster."}) ... if not iso_info: log.info( "No ISO download URLs found in infrastructure environments for cluster %s", cluster_id, ) - return "No ISO download URLs found for this cluster." + return json.dumps({"error": "No ISO download URLs found for this cluster."})assisted_service_mcp/src/tools/version_tools.py (1)
40-42: Doc/behavior mismatch and potential JSON serialization issue (already flagged)The existing review comment correctly identifies two issues:
- Passing
Truetoget_openshift_versionsreturns only latest versions, contradicting the docstring's claim of a "complete list"- The result is a
models.OpenshiftVersionsobject that may not be JSON-serializable without calling.to_dict()tests/test_metrics.py (1)
1-45: Missing FastAPI dependency (already flagged)The existing review comment correctly identifies that
fastapiis not in test dependencies, causingModuleNotFoundErrorduring test collection. FastAPI should be added to test/dev dependencies.
🧹 Nitpick comments (12)
tests/test_helpers.py (1)
12-48: Optional: Consider removing unnecessary async decorators.The test methods are marked with
@pytest.mark.asyncioand defined asasync def, but none of them containawaitstatements or async operations. These can be simplified to regular synchronous test methods for clarity and reduced overhead.Example for one test method:
- @pytest.mark.asyncio - async def test_get_platform_model_default(self) -> None: + def test_get_platform_model_default(self) -> None: """Test get_platform_model with None or empty string returns baremetal."""Apply similar changes to the other test methods. This is not blocking as the tests are passing, but would remove unnecessary complexity.
assisted_service_mcp/src/logger.py (3)
158-161: Use removeHandler() for clearer handler cleanup.The current pattern closes handlers but doesn't explicitly remove them from the logger before clearing the list. While assigning an empty list works, using
removeHandler()is more explicit and aligned with the logging API.Apply this diff for clearer intent:
# Reset handlers to prevent duplicates on reconfiguration - for handler in log.handlers: + for handler in log.handlers[:]: # Iterate over copy handler.close() - log.handlers = [] + log.removeHandler(handler)
28-63: Consider documenting the dict vs. object pattern asymmetry.The filter handles two distinct patterns:
- Dict keys with underscore prefix:
'_pull_secret','_ssh_public_key', etc.- Object attributes without prefix:
pull_secret,ssh_public_key, etc.This asymmetry appears intentional but isn't documented. Adding a comment explaining these two data formats would help future maintainers understand why both patterns exist.
Add clarifying comments:
@staticmethod def _filter(s: str) -> str: - # Dict filter + # Dict filter - matches keys with underscore prefix (e.g., from API responses) s = re.sub(r"('_pull_secret':\s+)'(.*?)'", r"\g<1>'*** PULL_SECRET ***'", s) ... - # Object filter + # Object filter - matches attributes without prefix (e.g., from Python objects) def _redact_value(text: str, key: str, placeholder: str) -> str:
93-95: Redundant third-party logger configuration.These logger levels are set at module import time, and then again in
configure_logging()(lines 155, 167). While not harmful, this redundancy could be eliminated ifconfigure_logging()is guaranteed to be called during application startup.If configure_logging() is always called (which appears to be the intent), you can remove these lines:
-logging.getLogger("requests").setLevel(logging.ERROR) -logging.getLogger("urllib3").setLevel(logging.ERROR) -logging.getLogger("asyncio").setLevel(logging.ERROR) - - def add_log_file_handler(logger: logging.Logger, filename: str) -> logging.FileHandler:assisted_service_mcp/utils/helpers.py (1)
12-35: Align the docstring with the return type.The docstring still says “readable string,” but the function returns a dict, which matches the type hints. Please adjust the wording so the documentation stays accurate.
assisted_service_mcp/src/tools/event_tools.py (1)
95-98: Inconsistent token retrieval patternIn
cluster_events(lines 46-47), the access token is retrieved and assigned to a variable before instantiatingInventoryClient, whereas here it's done inline. For consistency and easier debugging, consider using the same pattern throughout.Apply this diff for consistency:
- log.info("Retrieving events for host %s in cluster %s", host_id, cluster_id) - client = InventoryClient(get_access_token_func()) + log.info("Retrieving events for host %s in cluster %s", host_id, cluster_id) + access_token = get_access_token_func() + client = InventoryClient(access_token)assisted_service_mcp/src/mcp.py (1)
97-110: Consider: Inline schema in description may drift.Embedding
NMStateTemplateParams.model_json_schema()in the tool description is dynamic but creates tight coupling. If the function signature or parameter handling changes, this description may become inconsistent.Consider whether FastMCP can derive this from the wrapped function's signature automatically, or add a comment noting that this override must be manually kept in sync.
tests/test_integration_api.py (3)
8-18: Consider: Module reloading pattern is fragile.The pattern of deleting modules from
sys.modulesand reloading them (lines 15-16) works but is fragile. Module-level state, singletons, or import-time side effects may not reset cleanly. If tests exhibit flakiness or state pollution, consider using subprocess-based test isolation or dependency injection for transport configuration.For now, this is acceptable for integration tests.
21-36: Test workaround: Consider app initialization pattern.The
ensure_metrics_routehelper (lines 21-36) adds the/metricsroute because it's normally added inmain(). The complex route path extraction (lines 27-34) with nestedgetattr/hasattrcalls is defensive but fragile.Consider whether tests should use a helper that calls the full app initialization sequence, or whether the API module should expose a "configure_routes" method that tests can invoke.
52-59: Consider: Overly permissive liveness check.The test accepts status codes 200, 404, or 405 (line 59), which means it passes even if the root path has no handler. If the intent is to verify the server is running, this is adequate. However, if a proper liveness endpoint is needed, consider:
- Adding a dedicated
/healthor/livenessendpoint that always returns 200- Updating this test to expect only 200 from that endpoint
The current test name implies liveness but only verifies "server responded."
assisted_service_mcp/src/tools/cluster_tools.py (2)
254-254: Clarify: Specify "machine network subnet" for VIPs.Line 254 mentions "cluster's network subnet" which is ambiguous. VIPs should be in the machine network subnet (the external network where nodes communicate). Consider changing to:
"The IP addresses must be within the cluster's machine network subnet..."
This aligns with OpenShift terminology and prevents confusion with service/pod networks.
415-417: Move helper import to top-level Move the import of_get_cluster_infra_env_idto the module’s top-level imports inassisted_service_mcp/src/tools/cluster_tools.pyand remove the “avoid circular imports” comment, as no circular dependency exists.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (45)
Dockerfile(2 hunks)Makefile(2 hunks)README.md(2 hunks)assisted_service_mcp/__init__.py(1 hunks)assisted_service_mcp/src/__init__.py(1 hunks)assisted_service_mcp/src/api.py(1 hunks)assisted_service_mcp/src/logger.py(1 hunks)assisted_service_mcp/src/main.py(1 hunks)assisted_service_mcp/src/mcp.py(1 hunks)assisted_service_mcp/src/service_client/__init__.py(1 hunks)assisted_service_mcp/src/service_client/assisted_service_api.py(3 hunks)assisted_service_mcp/src/service_client/exceptions.py(1 hunks)assisted_service_mcp/src/service_client/helpers.py(1 hunks)assisted_service_mcp/src/settings.py(1 hunks)assisted_service_mcp/src/tools/__init__.py(1 hunks)assisted_service_mcp/src/tools/cluster_tools.py(1 hunks)assisted_service_mcp/src/tools/download_tools.py(1 hunks)assisted_service_mcp/src/tools/event_tools.py(1 hunks)assisted_service_mcp/src/tools/host_tools.py(1 hunks)assisted_service_mcp/src/tools/network_tools.py(1 hunks)assisted_service_mcp/src/tools/shared_helpers.py(1 hunks)assisted_service_mcp/src/tools/version_tools.py(1 hunks)assisted_service_mcp/src/utils/static_net/config.py(2 hunks)assisted_service_mcp/src/utils/static_net/template.py(5 hunks)assisted_service_mcp/utils/__init__.py(1 hunks)assisted_service_mcp/utils/auth.py(1 hunks)assisted_service_mcp/utils/helpers.py(1 hunks)integration_test/performance/README.md(1 hunks)pyproject.toml(4 hunks)pyrightconfig.json(1 hunks)server.py(0 hunks)service_client/logger.py(0 hunks)tests/test_api.py(1 hunks)tests/test_assisted_service_api.py(4 hunks)tests/test_auth.py(1 hunks)tests/test_helpers.py(1 hunks)tests/test_integration_api.py(1 hunks)tests/test_logger_filter.py(1 hunks)tests/test_mcp.py(1 hunks)tests/test_metrics.py(1 hunks)tests/test_server.py(0 hunks)tests/test_settings.py(1 hunks)tests/test_shared_helpers.py(1 hunks)tests/test_static_net.py(3 hunks)tests/test_tools_module.py(1 hunks)
💤 Files with no reviewable changes (3)
- server.py
- service_client/logger.py
- tests/test_server.py
✅ Files skipped from review due to trivial changes (4)
- assisted_service_mcp/src/init.py
- integration_test/performance/README.md
- assisted_service_mcp/utils/init.py
- assisted_service_mcp/src/tools/init.py
🚧 Files skipped from review as they are similar to previous changes (18)
- assisted_service_mcp/src/service_client/init.py
- pyproject.toml
- tests/test_static_net.py
- tests/test_settings.py
- tests/test_shared_helpers.py
- pyrightconfig.json
- assisted_service_mcp/src/tools/host_tools.py
- assisted_service_mcp/src/service_client/helpers.py
- tests/test_assisted_service_api.py
- tests/test_auth.py
- assisted_service_mcp/src/api.py
- assisted_service_mcp/src/service_client/assisted_service_api.py
- assisted_service_mcp/src/tools/network_tools.py
- tests/test_api.py
- tests/test_logger_filter.py
- assisted_service_mcp/utils/auth.py
- assisted_service_mcp/src/settings.py
- tests/test_tools_module.py
🧰 Additional context used
🧬 Code graph analysis (11)
tests/test_helpers.py (1)
assisted_service_mcp/src/service_client/helpers.py (1)
Helpers(7-38)
assisted_service_mcp/src/main.py (2)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)initiate_metrics(44-48)assisted_service_mcp/src/mcp.py (1)
list_tools_sync(151-163)
assisted_service_mcp/src/tools/download_tools.py (3)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (3)
list_infra_envs(218-236)get_infra_env_download_url(516-540)get_presigned_for_cluster_credentials(484-513)assisted_service_mcp/utils/helpers.py (1)
format_presigned_url(11-36)
tests/test_integration_api.py (1)
assisted_service_mcp/src/metrics/metrics.py (1)
metrics(69-71)
assisted_service_mcp/src/tools/event_tools.py (2)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (2)
InventoryClient(26-540)get_events(156-197)
assisted_service_mcp/src/tools/version_tools.py (3)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (4)
InventoryClient(26-540)get_openshift_versions(389-407)get_operator_bundles(410-420)add_operator_bundle_to_cluster(423-450)tests/test_tools_module.py (2)
to_str(444-445)to_str(488-489)
tests/test_metrics.py (1)
assisted_service_mcp/src/metrics/metrics.py (3)
metrics(69-71)initiate_metrics(44-48)track_tool_usage(51-65)
assisted_service_mcp/src/tools/cluster_tools.py (4)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (8)
InventoryClient(26-540)get_cluster(116-140)list_clusters(143-153)create_cluster(239-278)create_infra_env(281-303)update_cluster(330-368)install_cluster(371-386)update_infra_env(306-327)assisted_service_mcp/src/service_client/helpers.py (1)
Helpers(7-38)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
assisted_service_mcp/src/mcp.py (4)
assisted_service_mcp/utils/auth.py (2)
get_offline_token(10-45)get_access_token(48-114)assisted_service_mcp/src/tools/cluster_tools.py (5)
cluster_info(14-55)list_clusters(59-100)create_cluster(104-228)set_cluster_vips(232-286)install_cluster(334-372)assisted_service_mcp/src/tools/network_tools.py (4)
validate_nmstate_yaml(22-55)generate_nmstate_yaml(59-102)alter_static_network_config_nmstate_for_host(106-175)list_static_network_config(179-221)assisted_service_mcp/src/utils/static_net/template.py (1)
NMStateTemplateParams(102-118)
tests/test_mcp.py (1)
assisted_service_mcp/src/mcp.py (2)
AssistedServiceMCPServer(26-163)list_tools_sync(151-163)
assisted_service_mcp/src/tools/shared_helpers.py (1)
assisted_service_mcp/src/service_client/assisted_service_api.py (2)
InventoryClient(26-540)list_infra_envs(218-236)
🪛 GitHub Actions: Python linter
assisted_service_mcp/src/logger.py
[warning] 150-150: W0603: Using the global statement (global-statement)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Red Hat Konflux / assisted-service-mcp-saas-main-on-pull-request
🔇 Additional comments (17)
tests/test_helpers.py (1)
6-6: LGTM! Import path correctly updated for the new module structure.The import path change aligns with the refactoring objectives to organize code under
assisted_service_mcp/src/.assisted_service_mcp/src/service_client/exceptions.py (1)
12-12: LGTM! Import path correctly updated.The import path has been properly updated to use the centralized logger module, aligning with the new modular architecture.
assisted_service_mcp/src/utils/static_net/template.py (4)
14-17: LGTM! Field description now explicit.The refactoring separates the default value from the description, making the field definition clearer and more maintainable. This follows Pydantic best practices.
66-69: LGTM! Appropriate validation for bond interfaces.Adding
min_length=2ensures that bond interfaces have the minimum required number of ports. This catches configuration errors early at the validation layer.
85-85: LGTM! Correct VLAN ID range validation.The range constraint (1-4094) correctly excludes reserved VLAN IDs (0 and 4095) per IEEE 802.1Q standard.
124-124: LGTM! More explicit template rendering.Unpacking
model_dump()as keyword arguments is more explicit and aligns with typical Jinja2 usage patterns. The template already uses dot notation to access nested fields, which works with both approaches.assisted_service_mcp/src/mcp.py (4)
33-54: LGTM! Clean initialization with auth closure pattern.The lambda closures at lines 46-49 elegantly bind auth helpers to the MCP instance, enabling tools to access request-specific auth without direct context dependencies. Exception handling and logging are appropriate.
Minor note: Line 37's
(settings.TRANSPORT or "").lower()handles None/empty safely, but consider whether settings validation should enforce valid transport values earlier.
116-145: LGTM! Wrapper pattern cleanly injects auth dependencies.The signature manipulation (lines 134-143) correctly hides the injected
get_access_token_funcparameter from the tool's public interface. This allows tool functions to remain clean and testable while accessing request-specific auth at runtime.The
if len(params) >= 1guard at line 138 prevents errors if a parameterless function is wrapped, though all current tools have parameters.
147-149: LGTM! Straightforward async tool listing.
151-163: LGTM! Safe sync wrapper prevents nested event loops.The runtime check for an existing event loop (lines 153-157) prevents
RuntimeErrorfrom nestedasyncio.run()calls. The error message at lines 160-163 clearly guides users to use the async method in async contexts.tests/test_integration_api.py (1)
39-49: LGTM! Basic metrics exposition verification.The test confirms that the
/metricsendpoint returns 200 and contains Prometheus format indicators. Line 49's check for "HELP" or "# HELP" is appropriate for a basic smoke test.assisted_service_mcp/src/tools/cluster_tools.py (6)
13-55: LGTM! Well-documented cluster info retrieval.The comprehensive docstring with Examples (lines 29-32), Prerequisites (lines 34-36), and Related tools (lines 38-42) sections provides valuable context for LLM-based tool selection and usage. While these sections may seem verbose, they help the model understand when and how to use this tool.
58-100: LGTM! Appropriate response filtering.The function filters cluster details to essential fields (name, id, version, status) at lines 90-98. Using
getattrwith a default value foropenshift_version(line 94) safely handles potential missing attributes.
185-192: LGTM! Clear platform validation logic.The platform validation (lines 185-192) correctly enforces that single-node clusters must use platform 'none' (line 187), while defaulting multi-node clusters to 'baremetal'. The explicit
single_node is Truecheck is clearer than implicit truthiness.
289-330: LGTM! Platform update with clear documentation.The function correctly updates the cluster platform. The docstring clearly explains platform requirements for different cluster types (single-node requires 'none', multi-node supports baremetal/vsphere/oci/nutanix).
333-372: LGTM! Clear prerequisites for installation.The detailed Prerequisites section (lines 352-357) helps prevent premature installation attempts. For LLM-based tool usage, this context is valuable to ensure the model checks cluster readiness before triggering installation.
423-434: LGTM! Graceful partial success handling.The error handling at lines 423-427 and 429-434 provides partial success messages when the cluster update succeeds but InfraEnv operations fail. This approach is appropriate for LLM-based tools, as it returns actionable error context as strings rather than raising exceptions.
The function ensures the cluster SSH key is always updated (line 419), with InfraEnv updates as best-effort.
3a2c8d9 to
bea136b
Compare
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (7)
tests/test_logger_filter.py (2)
17-23: Same issue as noted intest_redact_object_style_single_quotes: consider verifying that original sensitive values are not present in the redacted output.
26-32: Same issue as noted intest_redact_object_style_single_quotes: consider verifying that original sensitive values are not present in the redacted output.pyrightconfig.json (1)
12-12: Re-enableuseLibraryCodeForTypesfor comprehensive type coverage.This setting disables Pyright's ability to infer types from runtime library code when stubs are missing. Many dependencies (FastAPI, requests, prometheus-client) lack complete stubs, so with this flag set to
false, their APIs will be treated asUnknown, hiding potential type errors in integrations.As noted in the previous review, please set this to
trueor remove the property:- "useLibraryCodeForTypes": false + "useLibraryCodeForTypes": trueassisted_service_mcp/src/settings.py (1)
147-177: Rename parameter to avoid shadowing the globalsettings.The function parameter name
settingsshadows the module-levelsettingsinstance (line 181), triggering theredefined-outer-namewarning (W0621) shown in pipeline failures.Rename the parameter to avoid the conflict:
-def validate_config(settings: Settings) -> None: +def validate_config(config: Settings) -> None: """Validate configuration settings. Performs validation to ensure required settings are present and values are within acceptable ranges. Args: - settings: Settings instance to validate. + config: Settings instance to validate. Raises: ValueError: If required configuration is missing or invalid. """ # Validate port range - if not 1024 <= settings.MCP_PORT <= 65535: + if not 1024 <= config.MCP_PORT <= 65535: raise ValueError( - f"MCP_PORT must be between 1024 and 65535, got {settings.MCP_PORT}" + f"MCP_PORT must be between 1024 and 65535, got {config.MCP_PORT}" ) # Validate log level valid_log_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] - if settings.LOGGING_LEVEL.upper() not in valid_log_levels: + if config.LOGGING_LEVEL.upper() not in valid_log_levels: raise ValueError( - f"LOGGING_LEVEL must be one of {valid_log_levels}, got {settings.LOGGING_LEVEL}" + f"LOGGING_LEVEL must be one of {valid_log_levels}, got {config.LOGGING_LEVEL}" ) # Validate transport protocol valid_transports = ["sse", "streamable-http"] - if settings.TRANSPORT not in valid_transports: + if config.TRANSPORT not in valid_transports: raise ValueError( - f"TRANSPORT must be one of {valid_transports}, got {settings.TRANSPORT}" + f"TRANSPORT must be one of {valid_transports}, got {config.TRANSPORT}" )assisted_service_mcp/src/tools/download_tools.py (1)
47-95: Standardize return format to JSON for all code paths.The function returns plain strings on error/no-results (lines 54, 58, 92) but JSON on success (line 95), creating an inconsistent interface that breaks client-side JSON parsing. The
cluster_credentials_download_urlfunction below (lines 98-167) demonstrates the correct pattern by consistently returning JSON.Apply this diff to standardize all returns to JSON:
try: token = get_access_token_func() client = InventoryClient(token) infra_envs = await client.list_infra_envs(cluster_id) except Exception as e: log.error("Failed to retrieve infrastructure environments: %s", e) - return f"Error retrieving ISO URLs: {str(e)}" + return json.dumps({"error": f"Error retrieving ISO URLs: {str(e)}"}) if not infra_envs: log.info("No infrastructure environments found for cluster %s", cluster_id) - return "No ISO download URLs found for this cluster." + return json.dumps({"error": "No ISO download URLs found for this cluster."}) ... if not iso_info: log.info( "No ISO download URLs found in infrastructure environments for cluster %s", cluster_id, ) - return "No ISO download URLs found for this cluster." + return json.dumps({"error": "No ISO download URLs found for this cluster."}) log.info("Returning %d ISO URLs for cluster %s", len(iso_info), cluster_id) return json.dumps(iso_info)assisted_service_mcp/src/tools/cluster_tools.py (2)
7-11: Move helper import to module level (avoid pylint C0415).importing inside functions is unnecessary here; no circular dependency is evident.
Apply this diff to add the import at the top:
from assisted_service_mcp.src.metrics import track_tool_usage from assisted_service_mcp.src.service_client.assisted_service_api import InventoryClient from assisted_service_mcp.src.service_client.helpers import Helpers from assisted_service_mcp.src.logger import log +from assisted_service_mcp.src.tools.shared_helpers import _get_cluster_infra_env_id
415-417: Remove in-function import (move to top-level).Replace the local import with the module-level import suggested above.
- # Import helper function here to avoid circular imports - from assisted_service_mcp.src.tools.shared_helpers import _get_cluster_infra_env_id
🧹 Nitpick comments (10)
tests/test_logger_filter.py (3)
8-14: Verify that original sensitive values are removed.The test confirms that redacted placeholders appear in the output, but it doesn't verify that the original sensitive values (
abc123,ssh-rsa AAA, etc.) have been removed. This gap could allow false positives if the redaction logic fails to remove the original values.Consider adding assertions to verify the absence of original values:
def test_redact_object_style_single_quotes() -> None: original = "pull_secret='abc123' ssh_public_key='ssh-rsa AAA' vsphere_username='user' vsphere_password='pass'" redacted = filter_text(original) assert "pull_secret='*** PULL_SECRET ***'" in redacted assert "ssh_public_key='*** SSH_KEY ***'" in redacted assert "vsphere_username='*** VSPHERE_USER ***'" in redacted assert "vsphere_password='*** VSPHERE_PASSWORD ***'" in redacted + assert "abc123" not in redacted + assert "ssh-rsa AAA" not in redacted + assert "user" not in redacted # or be more specific if 'user' appears elsewhere + assert "pass" not in redactedApply similar changes to the other test functions.
35-41: Good coverage of whitespace preservation.The test correctly verifies that various whitespace patterns (spaces and tabs) around the equals sign are preserved during redaction. However, like the other tests, consider adding assertions to verify that the original sensitive values are not present in the output.
1-41: Consider adding tests for dictionary-style redaction patterns.The current tests only cover object-style patterns (e.g.,
pull_secret='value'). However, theSensitiveFormatter._filtermethod also handles dictionary-style patterns (e.g.,'_pull_secret': 'value') according to the implementation inassisted_service_mcp/src/logger.py. Consider adding test coverage for these patterns to ensure comprehensive validation.Example test to add:
def test_redact_dict_style() -> None: original = "{'_pull_secret': 'abc123', '_ssh_public_key': 'ssh-rsa AAA'}" redacted = filter_text(original) assert "'_pull_secret': '*** PULL_SECRET ***'" in redacted assert "'_ssh_public_key': '*** SSH_KEY ***'" in redacted assert "abc123" not in redacted assert "ssh-rsa AAA" not in redactedtests/test_api.py (2)
18-21: Consider verifying transport-specific configuration.The test validates that
appandserverattributes exist, but doesn't verify they're configured for SSE. You could enhance this by checkingserver.mcp.stateless_httpis False for SSE.Optional enhancement:
def test_api_uses_sse_when_configured() -> None: api_mod = import_api_with_transport("sse") assert hasattr(api_mod, "app") assert hasattr(api_mod, "server") + # Verify SSE configuration (stateless_http should be False) + assert api_mod.server.mcp.stateless_http is False
24-27: Consider verifying transport-specific configuration.Similar to the SSE test, you could verify that
stateless_httpis True for streamable-http transport.Optional enhancement:
def test_api_uses_streamable_http_when_configured() -> None: api_mod = import_api_with_transport("streamable-http") assert hasattr(api_mod, "app") assert hasattr(api_mod, "server") + # Verify streamable-http configuration (stateless_http should be True) + assert api_mod.server.mcp.stateless_http is Truetests/test_integration_api.py (1)
21-36: Consider simplifying the route lookup.The defensive route lookup with multiple
getattrcalls works but is quite complex. You could simplify this while maintaining safety.Optional simplification:
def ensure_metrics_route(app) -> None: # type: ignore[no-untyped-def] # Attach /metrics route for the test (normally added in main()) from assisted_service_mcp.src.metrics import ( metrics as metrics_endpoint, ) # pylint: disable=import-outside-toplevel - routes = { - ( - getattr(getattr(r, "path", None), "strip", str)("/") - if hasattr(r, "path") - else getattr(r, "path", None) - ): r - for r in getattr(app, "routes", []) - } + routes = {} + for r in getattr(app, "routes", []): + path = getattr(r, "path", None) + if path: + normalized_path = path.strip("/") if isinstance(path, str) else path + routes[normalized_path] = r + if "/metrics" not in routes: app.add_route("/metrics", metrics_endpoint)assisted_service_mcp/src/tools/cluster_tools.py (4)
252-256: Clarify “cluster subnet” wording to “machine network subnet”.Reduces confusion with internal SDN networks.
- platforms. VIPs are NOT needed for single-node clusters or clusters on 'none' or 'oci' - platforms. The IP addresses must be within the cluster's network subnet, not assigned to + platforms. VIPs are NOT needed for single-node clusters or clusters on 'none' or 'oci' + platforms. The IP addresses must be within the cluster's machine network subnet, not assigned to any physical host, and reachable from all cluster nodes.
432-434: Include error details in user-facing partial-failure message.Expose the exception reason to aid users without needing logs.
- except Exception as e: + except Exception as e: log.error("Failed to update InfraEnv %s: %s", infra_env_id, str(e)) - return f"Cluster key updated, but boot image key update failed. New cluster: {result.to_str()}" + return f"Cluster key updated, but boot image key update failed: {str(e)}. New cluster: {result.to_str()}"
55-55: Optional: standardize return format (prefer JSON).Currently, cluster_info returns to_str() while list_clusters returns JSON. Consider returning JSON from cluster_info for consistency.
Example change (if models support to_dict()):
- return result.to_str() + return json.dumps(result.to_dict())Confirm that result exposes to_dict(); otherwise we can build a minimal projection similar to list_clusters.
Also applies to: 100-100
132-138: Tighten input validation for cpu_architecture and platform
- Restrict cpu_architecture to
Literal["x86_64","arm64","ppc64le","s390x","multi"]and update Field description to match (drop “aarch64”).- After the existing single-node check, add:
if not single_node and platform == "none": return "Platform 'none' is only valid for single-node clusters"Apply these diffs:
--- a/assisted_service_mcp/src/tools/cluster_tools.py +++ b/assisted_service_mcp/src/tools/cluster_tools.py @@ -1,5 +1,5 @@ -from typing import Annotated, Callable +from typing import Annotated, Callable, Literal @@ -132,8 +132,9 @@ - cpu_architecture: Annotated[ - str, + cpu_architecture: Annotated[ + Literal["x86_64", "arm64", "ppc64le", "s390x", "multi"], Field( - default="x86_64", - description="CPU architecture for the cluster. Valid options: x86_64 (default), aarch64, arm64, ppc64le, s390x.", + default="x86_64", + description="CPU architecture for the cluster. Valid options: x86_64 (default), arm64, ppc64le, s390x, multi.", ), ] = "x86_64" @@ -185,6 +186,9 @@ if single_node is True and platform != "none": return "Platform must be set to 'none' for single-node clusters" + if not single_node and platform == "none": + return "Platform 'none' is only valid for single-node clusters" + else: platform = "baremetal" if single_node is True:
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (45)
Dockerfile(2 hunks)Makefile(2 hunks)README.md(2 hunks)assisted_service_mcp/__init__.py(1 hunks)assisted_service_mcp/src/__init__.py(1 hunks)assisted_service_mcp/src/api.py(1 hunks)assisted_service_mcp/src/logger.py(1 hunks)assisted_service_mcp/src/main.py(1 hunks)assisted_service_mcp/src/mcp.py(1 hunks)assisted_service_mcp/src/service_client/__init__.py(1 hunks)assisted_service_mcp/src/service_client/assisted_service_api.py(3 hunks)assisted_service_mcp/src/service_client/exceptions.py(1 hunks)assisted_service_mcp/src/service_client/helpers.py(1 hunks)assisted_service_mcp/src/settings.py(1 hunks)assisted_service_mcp/src/tools/__init__.py(1 hunks)assisted_service_mcp/src/tools/cluster_tools.py(1 hunks)assisted_service_mcp/src/tools/download_tools.py(1 hunks)assisted_service_mcp/src/tools/event_tools.py(1 hunks)assisted_service_mcp/src/tools/host_tools.py(1 hunks)assisted_service_mcp/src/tools/network_tools.py(1 hunks)assisted_service_mcp/src/tools/shared_helpers.py(1 hunks)assisted_service_mcp/src/tools/version_tools.py(1 hunks)assisted_service_mcp/src/utils/static_net/config.py(2 hunks)assisted_service_mcp/src/utils/static_net/template.py(5 hunks)assisted_service_mcp/utils/__init__.py(1 hunks)assisted_service_mcp/utils/auth.py(1 hunks)assisted_service_mcp/utils/helpers.py(1 hunks)integration_test/performance/README.md(1 hunks)pyproject.toml(4 hunks)pyrightconfig.json(1 hunks)server.py(0 hunks)service_client/logger.py(0 hunks)tests/test_api.py(1 hunks)tests/test_assisted_service_api.py(4 hunks)tests/test_auth.py(1 hunks)tests/test_helpers.py(1 hunks)tests/test_integration_api.py(1 hunks)tests/test_logger_filter.py(1 hunks)tests/test_mcp.py(1 hunks)tests/test_metrics.py(1 hunks)tests/test_server.py(0 hunks)tests/test_settings.py(1 hunks)tests/test_shared_helpers.py(1 hunks)tests/test_static_net.py(3 hunks)tests/test_tools_module.py(1 hunks)
💤 Files with no reviewable changes (3)
- server.py
- service_client/logger.py
- tests/test_server.py
✅ Files skipped from review due to trivial changes (2)
- assisted_service_mcp/src/tools/init.py
- assisted_service_mcp/src/init.py
🚧 Files skipped from review as they are similar to previous changes (21)
- integration_test/performance/README.md
- README.md
- assisted_service_mcp/src/tools/event_tools.py
- tests/test_mcp.py
- assisted_service_mcp/init.py
- tests/test_helpers.py
- assisted_service_mcp/src/tools/network_tools.py
- Dockerfile
- assisted_service_mcp/src/tools/host_tools.py
- assisted_service_mcp/src/tools/version_tools.py
- assisted_service_mcp/src/service_client/helpers.py
- assisted_service_mcp/src/utils/static_net/config.py
- assisted_service_mcp/src/logger.py
- assisted_service_mcp/src/service_client/assisted_service_api.py
- assisted_service_mcp/src/api.py
- tests/test_auth.py
- assisted_service_mcp/src/utils/static_net/template.py
- assisted_service_mcp/src/service_client/exceptions.py
- assisted_service_mcp/src/service_client/init.py
- tests/test_static_net.py
- assisted_service_mcp/utils/helpers.py
🧰 Additional context used
🧬 Code graph analysis (12)
assisted_service_mcp/src/main.py (2)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)initiate_metrics(44-48)assisted_service_mcp/src/mcp.py (1)
list_tools_sync(151-163)
tests/test_assisted_service_api.py (2)
assisted_service_mcp/src/service_client/assisted_service_api.py (1)
InventoryClient(26-540)assisted_service_mcp/src/service_client/exceptions.py (1)
AssistedServiceAPIError(15-16)
tests/test_integration_api.py (1)
assisted_service_mcp/src/metrics/metrics.py (1)
metrics(69-71)
assisted_service_mcp/src/mcp.py (4)
assisted_service_mcp/utils/auth.py (2)
get_offline_token(10-45)get_access_token(48-114)assisted_service_mcp/src/tools/cluster_tools.py (1)
cluster_info(14-55)assisted_service_mcp/src/tools/network_tools.py (1)
generate_nmstate_yaml(59-102)assisted_service_mcp/src/utils/static_net/template.py (1)
NMStateTemplateParams(102-118)
assisted_service_mcp/utils/auth.py (2)
assisted_service_mcp/src/settings.py (1)
get_setting(184-193)tests/test_auth.py (1)
get_context(22-23)
tests/test_logger_filter.py (1)
assisted_service_mcp/src/logger.py (2)
SensitiveFormatter(15-76)_filter(28-63)
tests/test_tools_module.py (9)
assisted_service_mcp/src/mcp.py (1)
AssistedServiceMCPServer(26-163)assisted_service_mcp/src/service_client/assisted_service_api.py (14)
update_cluster(330-368)create_cluster(239-278)install_cluster(371-386)get_events(156-197)list_infra_envs(218-236)get_infra_env_download_url(516-540)get_presigned_for_cluster_credentials(484-513)update_host(453-481)add_operator_bundle_to_cluster(423-450)get_operator_bundles(410-420)get_infra_env(200-215)get_openshift_versions(389-407)create_infra_env(281-303)update_infra_env(306-327)assisted_service_mcp/src/tools/cluster_tools.py (5)
set_cluster_platform(290-330)set_cluster_vips(232-286)create_cluster(104-228)install_cluster(334-372)set_cluster_ssh_key(376-439)assisted_service_mcp/src/tools/event_tools.py (2)
cluster_events(12-53)host_events(57-112)assisted_service_mcp/src/tools/download_tools.py (2)
cluster_iso_download_url(14-95)cluster_credentials_download_url(99-167)assisted_service_mcp/src/tools/host_tools.py (1)
set_host_role(13-65)assisted_service_mcp/src/tools/version_tools.py (3)
add_operator_bundle_to_cluster(86-141)list_operator_bundles(49-82)list_versions(13-45)assisted_service_mcp/src/tools/network_tools.py (4)
validate_nmstate_yaml(22-55)alter_static_network_config_nmstate_for_host(106-175)list_static_network_config(179-221)generate_nmstate_yaml(59-102)assisted_service_mcp/src/utils/static_net/template.py (2)
NMStateTemplateParams(102-118)EthernetInterfaceParams(37-44)
assisted_service_mcp/src/tools/shared_helpers.py (2)
assisted_service_mcp/src/service_client/assisted_service_api.py (2)
InventoryClient(26-540)list_infra_envs(218-236)tests/test_assisted_service_api.py (1)
client(32-37)
assisted_service_mcp/src/tools/cluster_tools.py (4)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (8)
InventoryClient(26-540)get_cluster(116-140)list_clusters(143-153)create_cluster(239-278)create_infra_env(281-303)update_cluster(330-368)install_cluster(371-386)update_infra_env(306-327)assisted_service_mcp/src/service_client/helpers.py (1)
Helpers(7-38)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
tests/test_shared_helpers.py (2)
assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)assisted_service_mcp/src/service_client/assisted_service_api.py (1)
list_infra_envs(218-236)
assisted_service_mcp/src/tools/download_tools.py (3)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (3)
list_infra_envs(218-236)get_infra_env_download_url(516-540)get_presigned_for_cluster_credentials(484-513)assisted_service_mcp/utils/helpers.py (1)
format_presigned_url(11-36)
tests/test_metrics.py (1)
assisted_service_mcp/src/metrics/metrics.py (3)
metrics(69-71)initiate_metrics(44-48)track_tool_usage(51-65)
🪛 GitHub Actions: Python linter
tests/test_logger_filter.py
[warning] 1-1: R0801: Similar lines in 2 files.
assisted_service_mcp/src/settings.py
[warning] 147-147: Redefining name 'settings' from outer scope (W0621).
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Red Hat Konflux / assisted-service-mcp-saas-main-on-pull-request
🔇 Additional comments (42)
assisted_service_mcp/utils/__init__.py (1)
1-1: LGTM! Clean package initializer.The minimal
__init__.pywith a clear docstring follows modern Python conventions. This approach keeps the package structure clean and avoids potential circular import issues while allowing explicit imports from submodules.tests/test_logger_filter.py (2)
4-5: LGTM!The helper function provides a clean interface for testing the protected
_filtermethod. Usingprotected-accessin tests to verify internal behavior is an acceptable practice.
1-41: Address code duplication warning from the linter.The pipeline reports similar lines in 2 files (R0801). While some duplication in tests is acceptable, consider whether the repeated assertion patterns could be reduced through parameterized tests or helper functions.
Example refactoring using pytest's parametrize:
import pytest @pytest.mark.parametrize("quote_char,expected_quote", [ ("'", "'"), ('"', '"'), ("", ""), ]) def test_redact_object_style_quotes(quote_char: str, expected_quote: str) -> None: if quote_char: original = f"pull_secret={quote_char}abc123{quote_char} ssh_public_key={quote_char}ssh-rsa AAA{quote_char}" assert f"pull_secret={expected_quote}*** PULL_SECRET ***{expected_quote}" in filter_text(original) else: original = "pull_secret=abc123 ssh_public_key=ssh-rsaAAA" assert "pull_secret=*** PULL_SECRET ***" in filter_text(original)However, this refactoring may reduce readability. Evaluate whether the duplication is acceptable given the clarity of the current test structure.
Makefile (2)
18-18: LGTM! Entry point correctly updated.The run-local target now uses the new modular entry point
python -m assisted_service_mcp.src.main, which aligns with the PR's refactoring objectives.
33-33: LGTM! Coverage targets updated correctly.The test-coverage target now covers the new
assisted_service_mcppackage structure, replacing the oldserverpackage coverage.pyproject.toml (4)
17-18: LGTM! Settings dependencies added.The addition of
pydantic-settings>=2.6.0andpython-dotenv>=1.0.0supports the new centralized configuration module introduced in this PR.
30-31: LGTM! Type stubs and dev dependencies properly configured.The
types-requestsdependency is now correctly placed in dev dependencies only (not duplicated in main), andfastapiis added to support the new API transport layer.
38-38: LGTM! FastAPI added to test dependencies.This resolves the pipeline failure where
test_metrics.pycouldn't importfastapi.testclient.
53-64: LGTM! Pylint configuration updated appropriately.The addition of
integration_testto ignore-paths andimport-outside-toplevelto the disable list are reasonable for the new modular structure and runtime import patterns.tests/test_metrics.py (2)
9-15: LGTM! Clean metrics endpoint test.The test correctly verifies that the
/metricsendpoint returns Prometheus-formatted output with HTTP 200.
18-45: LGTM! Comprehensive decorator test.The test validates both counter and histogram metrics, with the
>= 2.0threshold correctly accounting for theinitiate_metricsinitialization call (which increments both metrics to 1) plus the actual function call (incrementing to 2).tests/test_assisted_service_api.py (4)
12-13: LGTM! Imports updated to new package structure.The imports now correctly reference
assisted_service_mcp.src.service_clientpaths, aligning with the modular refactoring.
61-70: LGTM! Settings patching updated correctly.The test now patches settings constants via the
assisted_service_mcp.src.settings.settingspath instead of environment variables, which is the correct approach for the new centralized configuration.
116-118: LGTM! Pull secret URL patching updated.Consistent with other settings patches, now targeting the settings module.
141-141: LGTM! ApiClient patch path updated.The patch path now correctly references the new module location.
assisted_service_mcp/src/settings.py (4)
3-5: LGTM! Import organization is correct.The typing imports are now properly grouped together at the top of the file.
12-21: LGTM! Robust environment loading.The
load_dotenv()call with exception handling appropriately handles missing.envfiles and warns on unexpected errors without failing the application startup.
24-144: LGTM! Well-structured settings class.The Settings class uses Pydantic BaseSettings effectively with:
- Comprehensive field definitions with validation
- JSON schema metadata for documentation
- Appropriate defaults
- Frozen=False to allow test patching
184-193: LGTM! Clever workaround for test patching.The
get_settingfunction's use of__dict__access ensures that unittest.mock.patch can directly modify instance attributes, supporting flexible test scenarios.assisted_service_mcp/src/tools/download_tools.py (1)
98-167: LGTM! Consistent JSON return format.This function correctly returns JSON for all code paths (error, no-result, and success cases), making it easy for clients to parse responses reliably.
tests/test_shared_helpers.py (3)
7-12: LGTM! Clear success case test.The test correctly validates that
_get_cluster_infra_env_idreturns the ID from the first infrastructure environment.
15-20: LGTM! Proper error handling for empty list.The test verifies that a
ValueErroris raised when no infrastructure environments are found.
23-28: LGTM! Edge case for missing ID field.The test ensures that a
ValueErroris raised when an infrastructure environment lacks a valid ID field.assisted_service_mcp/src/tools/shared_helpers.py (1)
7-41: LGTM!The helper function is well-implemented with appropriate error handling for edge cases (no InfraEnv, multiple InfraEnvs, missing ID). The logging provides good observability, and the warning for multiple InfraEnvs is helpful for debugging unexpected configurations.
tests/test_api.py (1)
6-15: LGTM!The helper correctly implements the module reloading pattern with proper cache clearing and environment variable override.
assisted_service_mcp/src/main.py (1)
10-42: LGTM!The main entry point is well-structured with:
- Clear startup logging showing configuration
- Proper metrics initialization before server start
- Clean exception handling for interrupts and errors
- Guaranteed shutdown logging via finally block
The implementation follows best practices for service startup and shutdown.
assisted_service_mcp/utils/auth.py (2)
10-45: LGTM!The offline token retrieval follows a sensible fallback pattern (environment → request header) with clear error messages and appropriate logging at each step.
48-114: LGTM!The access token retrieval is well-implemented with:
- Appropriate fallback from Authorization header to SSO exchange
- Robust error handling for HTTP errors and malformed responses
- 30-second timeout on the SSO request preventing hangs
- Clear logging throughout the flow
tests/test_settings.py (4)
6-16: LGTM!The helper correctly implements settings reloading with environment overrides. The missing return type annotation is explicitly ignored, which is acceptable for test utilities.
19-27: LGTM!The test comprehensively validates default settings values, appropriately using
infor TRANSPORT since the default can be either valid option.
30-44: LGTM!The test properly validates that environment variables override defaults, covering the main configurable settings.
47-53: LGTM!The validation test correctly verifies that invalid TRANSPORT values raise a ValidationError with the expected message. The local import with pylint disable is appropriate for this test.
tests/test_tools_module.py (1)
8-514: LGTM!This is an excellent test suite with comprehensive coverage of the MCP tools. The tests follow a consistent pattern, use appropriate mocking, and cover both happy paths and edge cases (e.g., invalid platform for SNO clusters, partial failures in SSH key updates, network config edge cases). The structure makes the tests easy to understand and maintain.
assisted_service_mcp/src/mcp.py (4)
33-54: LGTM!The initialization is well-structured with proper transport configuration, lambda-bound auth helpers for the MCP instance, and comprehensive exception handling with logging.
56-114: LGTM!The tool registration is well-organized by functional area. The special handling for
generate_nmstate_yamlwith embedded schema in the description (line 108) is a nice touch for providing structured metadata to consumers.
116-145: LGTM!The tool wrapping approach elegantly solves the dependency injection problem while keeping tool function signatures clean. The signature manipulation correctly hides the injected auth parameter from consumers. The
type: ignorefor__signature__assignment is acceptable since this is a legitimate signature override.
151-163: LGTM!The synchronous wrapper properly detects running event loops to prevent
asyncio.run()nesting, with a clear error message guiding users to the async version when appropriate.tests/test_integration_api.py (3)
8-18: LGTM!The helper correctly implements module reloading with transport override, matching the pattern used in test_api.py.
39-49: LGTM!The test appropriately validates that the metrics endpoint is accessible and returns Prometheus-formatted metrics. The check for "HELP" indicators is a reasonable proxy for correct Prometheus exposition format.
52-59: LGTM!The basic liveness test appropriately validates that the API responds, accepting multiple status codes since the root endpoint behavior may vary by transport.
assisted_service_mcp/src/tools/cluster_tools.py (2)
51-55: LGTM: cluster_info flow is clean.Good logging, client usage, and async handling.
87-101: LGTM: list_clusters summary output.Efficient projection and JSON response; sensible fallback for missing openshift_version.
bea136b to
7017285
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (5)
assisted_service_mcp/src/tools/version_tools.py (1)
40-42: Address the unresolved doc/behavior mismatch and JSON serialization issue.The previous review comment correctly identifies that:
- Line 40 passes
True(only latest) but the docstring promises "complete list of OpenShift versions"- Line 42 may fail if
resultis not JSON-serializable (it's amodels.OpenshiftVersionsobject)Please apply the suggested fix from the previous review to resolve both issues:
- result = await client.get_openshift_versions(True) + result = await client.get_openshift_versions(False) log.info("Successfully retrieved OpenShift versions") - return json.dumps(result) + payload = result.to_dict() if hasattr(result, "to_dict") else result + return json.dumps(payload)assisted_service_mcp/src/logger.py (3)
79-90: Silence intentional import-outside-toplevel.The import inside the function is deliberate to avoid circular dependencies at module load time, but it triggers pylint C0415. Add a disable comment to keep CI green.
Apply this diff:
# Import here to avoid circular dependency at module load time - from assisted_service_mcp.src.settings import settings + from assisted_service_mcp.src.settings import settings # pylint: disable=import-outside-toplevel
143-144: Silence intentional import-outside-toplevel.The import inside the function is deliberate to avoid circular dependencies, but it triggers pylint C0415.
Apply this diff:
# Import inside function to avoid circular dependency - from assisted_service_mcp.src.settings import settings + from assisted_service_mcp.src.settings import settings # pylint: disable=import-outside-toplevel
160-173: Reconsider urllib3_logger handler setup.Line 160 assigns
urllib3_logger.handlers = [logging.NullHandler()], but lines 169 and 173 then add file and stream handlers to the same logger. NullHandler is meant to suppress output when no handlers are configured, not to coexist with real handlers. This creates an inconsistent configuration.Consider one of these approaches:
Option 1: Remove NullHandler if you want urllib3 logs output:
- urllib3_logger.handlers = [logging.NullHandler()] + urllib3_logger.handlers = []Option 2: Don't add handlers if you want to suppress urllib3 logs:
# Optional file logging if settings.LOG_TO_FILE: add_log_file_handler(target_logger, "assisted-service-mcp.log") - add_log_file_handler(urllib3_logger, "assisted-service-mcp.log") # Always add stream handlers add_stream_handler(target_logger) - add_stream_handler(urllib3_logger)assisted_service_mcp/src/tools/download_tools.py (1)
47-95: Standardize return format to JSON for consistency.The function returns plain strings for errors (line 54) and no-results cases (lines 58, 92), but returns JSON for success (line 95). This inconsistency breaks clients expecting JSON responses. In contrast,
cluster_credentials_download_urlreturns JSON consistently.Apply this diff to standardize all return paths to JSON:
try: token = get_access_token_func() client = InventoryClient(token) infra_envs = await client.list_infra_envs(cluster_id) except Exception as e: log.error("Failed to retrieve infrastructure environments: %s", e) - return f"Error retrieving ISO URLs: {str(e)}" + return json.dumps({"error": f"Error retrieving ISO URLs: {str(e)}"}) if not infra_envs: log.info("No infrastructure environments found for cluster %s", cluster_id) - return "No ISO download URLs found for this cluster." + return json.dumps({"error": "No ISO download URLs found for this cluster."}) ... if not iso_info: log.info( "No ISO download URLs found in infrastructure environments for cluster %s", cluster_id, ) - return "No ISO download URLs found for this cluster." + return json.dumps({"error": "No ISO download URLs found for this cluster."}) log.info("Returning %d ISO URLs for cluster %s", len(iso_info), cluster_id) return json.dumps(iso_info)
🧹 Nitpick comments (4)
tests/test_shared_helpers.py (1)
1-28: Consider testing the multiple InfraEnvs warning case.The tests provide good coverage of success and error paths. However, the warning case when
len(infra_envs) > 1(fromshared_helpers.pylines 22-27) is not tested.You can add:
@pytest.mark.asyncio async def test_get_cluster_infra_env_id_multiple_infra_envs() -> None: client = AsyncMock() client.list_infra_envs.return_value = [{"id": "ie-1"}, {"id": "ie-2"}] res = await _get_cluster_infra_env_id(client, "cid") assert res == "ie-1" # Should use the first oneassisted_service_mcp/src/tools/event_tools.py (1)
44-50: Minor style inconsistency in token retrieval.Lines 46-47 retrieve the token and create the client in two steps, while
host_events(line 97) does this in one line. Consider aligning the style for consistency.Apply this diff to make it consistent with
host_events:- try: - access_token = get_access_token_func() - client = InventoryClient(access_token) + try: + client = InventoryClient(get_access_token_func()) result = await client.get_events(cluster_id=cluster_id)tests/test_settings.py (1)
6-16: Add return type annotation to helper function.The function returns a Settings instance but lacks a return type annotation. Adding it improves type safety and IDE support.
Apply this diff to add the type annotation:
-def reload_settings_with_env(env: dict[str, str]): # type: ignore[no-untyped-def] +def reload_settings_with_env(env: dict[str, str]) -> "Settings": module_name = "assisted_service_mcp.src.settings"Note: You can import Settings at the top if you prefer, or use a string annotation as shown to avoid import-time dependencies.
assisted_service_mcp/src/tools/cluster_tools.py (1)
415-416: Move import of_get_cluster_infra_env_idto module level
The function-level import isn’t needed—relocate it alongside the other imports at the top ofcluster_tools.py.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (44)
Dockerfile(2 hunks)Makefile(2 hunks)README.md(2 hunks)assisted_service_mcp/__init__.py(1 hunks)assisted_service_mcp/src/__init__.py(1 hunks)assisted_service_mcp/src/api.py(1 hunks)assisted_service_mcp/src/logger.py(1 hunks)assisted_service_mcp/src/main.py(1 hunks)assisted_service_mcp/src/mcp.py(1 hunks)assisted_service_mcp/src/service_client/__init__.py(1 hunks)assisted_service_mcp/src/service_client/assisted_service_api.py(3 hunks)assisted_service_mcp/src/service_client/exceptions.py(1 hunks)assisted_service_mcp/src/service_client/helpers.py(1 hunks)assisted_service_mcp/src/settings.py(1 hunks)assisted_service_mcp/src/tools/__init__.py(1 hunks)assisted_service_mcp/src/tools/cluster_tools.py(1 hunks)assisted_service_mcp/src/tools/download_tools.py(1 hunks)assisted_service_mcp/src/tools/event_tools.py(1 hunks)assisted_service_mcp/src/tools/host_tools.py(1 hunks)assisted_service_mcp/src/tools/network_tools.py(1 hunks)assisted_service_mcp/src/tools/shared_helpers.py(1 hunks)assisted_service_mcp/src/tools/version_tools.py(1 hunks)assisted_service_mcp/src/utils/static_net/config.py(2 hunks)assisted_service_mcp/src/utils/static_net/template.py(5 hunks)assisted_service_mcp/utils/__init__.py(1 hunks)assisted_service_mcp/utils/auth.py(1 hunks)assisted_service_mcp/utils/helpers.py(1 hunks)integration_test/performance/README.md(1 hunks)pyproject.toml(4 hunks)pyrightconfig.json(1 hunks)server.py(0 hunks)tests/test_api.py(1 hunks)tests/test_assisted_service_api.py(4 hunks)tests/test_auth.py(1 hunks)tests/test_helpers.py(1 hunks)tests/test_integration_api.py(1 hunks)tests/test_logger_filter.py(1 hunks)tests/test_mcp.py(1 hunks)tests/test_metrics.py(1 hunks)tests/test_server.py(0 hunks)tests/test_settings.py(1 hunks)tests/test_shared_helpers.py(1 hunks)tests/test_static_net.py(3 hunks)tests/test_tools_module.py(1 hunks)
💤 Files with no reviewable changes (2)
- server.py
- tests/test_server.py
🚧 Files skipped from review as they are similar to previous changes (22)
- Dockerfile
- assisted_service_mcp/src/tools/shared_helpers.py
- assisted_service_mcp/src/main.py
- tests/test_mcp.py
- assisted_service_mcp/init.py
- pyrightconfig.json
- assisted_service_mcp/src/service_client/helpers.py
- assisted_service_mcp/src/tools/init.py
- integration_test/performance/README.md
- tests/test_helpers.py
- assisted_service_mcp/utils/auth.py
- assisted_service_mcp/src/tools/network_tools.py
- tests/test_auth.py
- assisted_service_mcp/src/init.py
- assisted_service_mcp/utils/helpers.py
- tests/test_static_net.py
- assisted_service_mcp/src/api.py
- assisted_service_mcp/utils/init.py
- tests/test_integration_api.py
- README.md
- pyproject.toml
- tests/test_tools_module.py
🧰 Additional context used
🧬 Code graph analysis (11)
tests/test_logger_filter.py (1)
assisted_service_mcp/src/logger.py (2)
SensitiveFormatter(15-76)_filter(28-63)
tests/test_assisted_service_api.py (2)
assisted_service_mcp/src/service_client/assisted_service_api.py (1)
InventoryClient(26-540)assisted_service_mcp/src/service_client/exceptions.py (1)
AssistedServiceAPIError(15-16)
assisted_service_mcp/src/tools/host_tools.py (4)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (2)
InventoryClient(26-540)update_host(453-481)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)tests/test_tools_module.py (2)
to_str(444-445)to_str(488-489)
assisted_service_mcp/src/tools/download_tools.py (3)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (4)
InventoryClient(26-540)list_infra_envs(218-236)get_infra_env_download_url(516-540)get_presigned_for_cluster_credentials(484-513)assisted_service_mcp/utils/helpers.py (1)
format_presigned_url(11-36)
assisted_service_mcp/src/tools/event_tools.py (2)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (2)
InventoryClient(26-540)get_events(156-197)
tests/test_shared_helpers.py (3)
assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)tests/test_assisted_service_api.py (1)
client(32-37)assisted_service_mcp/src/service_client/assisted_service_api.py (1)
list_infra_envs(218-236)
assisted_service_mcp/src/service_client/assisted_service_api.py (3)
assisted_service_mcp/src/settings.py (1)
get_setting(184-193)assisted_service_mcp/src/service_client/exceptions.py (1)
sanitize_exceptions(24-58)assisted_service_mcp/src/service_client/helpers.py (1)
Helpers(7-38)
assisted_service_mcp/src/tools/version_tools.py (3)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (3)
get_openshift_versions(389-407)get_operator_bundles(410-420)add_operator_bundle_to_cluster(423-450)tests/test_tools_module.py (2)
to_str(444-445)to_str(488-489)
assisted_service_mcp/src/tools/cluster_tools.py (4)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (8)
InventoryClient(26-540)get_cluster(116-140)list_clusters(143-153)create_cluster(239-278)create_infra_env(281-303)update_cluster(330-368)install_cluster(371-386)update_infra_env(306-327)assisted_service_mcp/src/service_client/helpers.py (1)
Helpers(7-38)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
tests/test_metrics.py (1)
assisted_service_mcp/src/metrics/metrics.py (3)
metrics(69-71)initiate_metrics(44-48)track_tool_usage(51-65)
assisted_service_mcp/src/mcp.py (3)
assisted_service_mcp/utils/auth.py (2)
get_offline_token(10-45)get_access_token(48-114)assisted_service_mcp/src/tools/network_tools.py (4)
validate_nmstate_yaml(22-55)generate_nmstate_yaml(59-102)alter_static_network_config_nmstate_for_host(106-175)list_static_network_config(179-221)assisted_service_mcp/src/utils/static_net/template.py (1)
NMStateTemplateParams(102-118)
🪛 GitHub Actions: Black
assisted_service_mcp/src/settings.py
[error] 1-1: Black formatting check failed. 1 file would be reformatted by Black. Run 'black .' to format. Step: 'uv tool run black --check .'
🪛 GitHub Actions: Python linter
tests/test_logger_filter.py
[warning] 1-1: R0801: Similar lines in 2 files
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Red Hat Konflux / assisted-service-mcp-saas-main-on-pull-request
🔇 Additional comments (16)
assisted_service_mcp/src/utils/static_net/config.py (2)
33-36: LGTM: Correct exception type for index operations.Changing from
ValueErrortoIndexErroraligns with Python conventions for index-related errors. Note this is a breaking change if callers were catchingValueErrorspecifically.
76-77: LGTM: Explicit validation improves error messaging.Adding an explicit check for
interfaces is Noneprovides a clear error message rather than letting it fail with aTypeErrorduring iteration.assisted_service_mcp/src/utils/static_net/template.py (3)
66-69: LGTM: Correct validation for bond interfaces.Adding
min_length=2correctly enforces that bonds require at least two port interfaces, catching configuration errors at the model level.
85-85: LGTM: Correct VLAN ID range validation.The constraint
ge=1, le=4094correctly enforces the valid IEEE 802.1Q VLAN ID range (0 and 4095 are reserved).
124-124: LGTM: Idiomatic Jinja2 template rendering.Using
**params.model_dump()to unpack the model dictionary is the idiomatic approach for passing Pydantic models to Jinja2 templates. The template correctly uses dot notation to access fields, which works seamlessly with both dictionaries and objects in Jinja2.tests/test_metrics.py (1)
9-15: LGTM! Well-isolated test.The test creates a fresh FastAPI app, uses TestClient correctly with a context manager, and validates the metrics endpoint without relying on global state.
assisted_service_mcp/src/service_client/__init__.py (1)
8-8: LGTM!The import path update to absolute import from
assisted_service_mcp.src.loggeraligns with the centralized logging introduced in this PR and is consistent with the module reorganization.assisted_service_mcp/src/tools/host_tools.py (1)
12-65: LGTM!The implementation is clean and follows the established patterns for MCP tools in this PR:
- Properly uses the authentication token factory pattern
- Leverages the shared helper for InfraEnv ID resolution
- Includes comprehensive docstring with examples and prerequisites
- Decorated with telemetry tracking
assisted_service_mcp/src/tools/event_tools.py (1)
11-112: LGTM!Both event retrieval functions are well-implemented with proper error handling, comprehensive docstrings, and telemetry tracking.
assisted_service_mcp/src/tools/version_tools.py (2)
48-82: LGTM!The
list_operator_bundlesimplementation correctly handles JSON serialization sinceget_operator_bundles()already returns a list of dicts.
85-141: LGTM!The
add_operator_bundle_to_clusterimplementation is correct with proper error handling and logging.assisted_service_mcp/src/service_client/exceptions.py (1)
12-12: LGTM!The import path update is consistent with the centralized logging module introduced in this PR.
tests/test_logger_filter.py (2)
8-41: LGTM!The tests provide comprehensive coverage of the redaction logic for various formatting scenarios (single quotes, double quotes, unquoted values, and spacing preservation).
1-6: Ignore R0801 warning for this test (no duplicate code detected).Likely an incorrect or invalid review comment.
tests/test_api.py (2)
18-27: LGTM!The tests correctly verify that the API module exposes the expected attributes (
appandserver) for both transport configurations.
6-15: Verify test isolation across test orderEnsure
import_api_with_transportdoesn’t leak state by running API tests in different orders locally, e.g.:pytest -c /dev/null tests/test_api.py::test_api_uses_streamable_http_when_configured tests/test_api.py::test_api_uses_sse_when_configured pytest -c /dev/null tests/test_api.py::test_api_uses_sse_when_configured tests/test_api.py::test_api_uses_streamable_http_when_configured
Makefile
Outdated
| uv run --group test pytest --cov=service_client --cov=assisted_service_mcp --cov-report=html --cov-report=term-missing | ||
|
|
There was a problem hiding this comment.
Fix coverage target module path. Coverage now lives under assisted_service_mcp.src.service_client, but the command still instruments the old top-level service_client. Pytest --cov skips modules it cannot import, so we silently lose coverage for the relocated code. Update the target to the new package path.
- uv run --group test pytest --cov=service_client --cov=assisted_service_mcp --cov-report=html --cov-report=term-missing
+ uv run --group test pytest --cov=assisted_service_mcp.src.service_client --cov=assisted_service_mcp --cov-report=html --cov-report=term-missingAs noted in the PR summary, service_client now lives under assisted_service_mcp/src.
🤖 Prompt for AI Agents
In Makefile around lines 33 to 34, the pytest coverage target still points to
the old top-level module "service_client" which no longer exists; change the
--cov argument from "service_client" to the relocated package path
"assisted_service_mcp.src.service_client" (i.e. replace --cov=service_client
with --cov=assisted_service_mcp.src.service_client) so pytest instruments the
moved code and coverage is collected correctly.
700a108 to
68d881d
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
assisted_service_mcp/src/service_client/assisted_service_api.py (1)
63-71: Avoid blocking the event loop when fetching the pull secret.requests.post is synchronous and currently runs on the event loop thread via the pull_secret property, risking stalls under load. Make the fetch asynchronous and update call sites.
@@ - @property - def pull_secret(self) -> str: - """Lazy-load the pull secret when first accessed.""" - if self._pull_secret is None: - self._pull_secret = self._get_pull_secret() - return self._pull_secret + async def get_pull_secret(self) -> str: + """Lazy-load the pull secret asynchronously when first accessed.""" + if self._pull_secret is None: + url = get_setting("PULL_SECRET_URL") + headers = {"Authorization": f"Bearer {self.access_token}"} + + def _fetch() -> str: + log.info("Fetching pull secret from %s", url) + response = requests.post(url, headers=headers, timeout=30) + response.raise_for_status() + log.info("Successfully fetched pull secret") + return response.text + + self._pull_secret = await asyncio.to_thread(_fetch) + return self._pull_secret @@ - def _get_pull_secret(self) -> str: - url = get_setting("PULL_SECRET_URL") - headers = {"Authorization": f"Bearer {self.access_token}"} - - try: - log.info("Fetching pull secret from %s", url) - response = requests.post(url, headers=headers, timeout=30) - response.raise_for_status() - log.info("Successfully fetched pull secret") - return response.text - except RequestException as e: - log.error("Error while fetching pull secret from %s: %s", url, str(e)) - raise + # (Optional) Keep this for compatibility if referenced elsewhere; otherwise remove. + # def _get_pull_secret(self) -> str: ... @@ - params = models.ClusterCreateParams( + params = models.ClusterCreateParams( name=name, openshift_version=version, - pull_secret=self.pull_secret, + pull_secret=await self.get_pull_secret(), **cluster_params, ) @@ - infra_env = models.InfraEnvCreateParams( - name=name, pull_secret=self.pull_secret, **infra_env_params - ) + infra_env = models.InfraEnvCreateParams( + name=name, + pull_secret=await self.get_pull_secret(), + **infra_env_params, + )If you prefer httpx.AsyncClient, I can provide that variant.
Also applies to: 70-82, 262-267, 294-299
♻️ Duplicate comments (7)
pyrightconfig.json (1)
12-12: Re-enable library code type inference.Setting useLibraryCodeForTypes to false hides types for many deps (FastAPI, requests, prometheus-client), weakening checks. Set it to true (or remove the key; default is true).
Apply:
- "useLibraryCodeForTypes": false + "useLibraryCodeForTypes": truetests/test_metrics.py (1)
18-45: Test isolation issue remains unresolved.The previous review's concerns about global REGISTRY state contamination have not been addressed. The test still:
- Uses the literal "foo_tool" name which can collide with other tests
- Asserts
>= 2.0instead of exact equality- Does not clean up or isolate REGISTRY state
This can cause flaky tests when running the suite multiple times or in parallel.
Consider applying the previously suggested fix to use a unique tool name per test and assert exact values.
assisted_service_mcp/src/settings.py (1)
153-156: Docstring parameter name mismatch (cfg vs settings).Update Args section to match the parameter name.
- Args: - settings: Settings instance to validate. + Args: + cfg: Settings instance to validate.assisted_service_mcp/src/logger.py (1)
154-174: Avoid mixing NullHandler with active handlers on urllib3 loggerAssigning NullHandler and then adding file/stream handlers is contradictory.
- for handler in urllib3_logger.handlers: - handler.close() - urllib3_logger.handlers = [logging.NullHandler()] + for handler in urllib3_logger.handlers: + handler.close() + urllib3_logger.handlers = [] @@ - if settings.LOG_TO_FILE: - add_log_file_handler(target_logger, "assisted-service-mcp.log") - add_log_file_handler(urllib3_logger, "assisted-service-mcp.log") + if settings.LOG_TO_FILE: + add_log_file_handler(target_logger, "assisted-service-mcp.log") + add_log_file_handler(urllib3_logger, "assisted-service-mcp.log") @@ - add_stream_handler(target_logger) - add_stream_handler(urllib3_logger) + add_stream_handler(target_logger) + add_stream_handler(urllib3_logger)assisted_service_mcp/src/tools/download_tools.py (1)
47-55: Standardize return type to JSON for error/no-results pathscluster_iso_download_url returns plain strings on error/empty but JSON on success. This breaks clients expecting JSON. Align all branches to JSON, like cluster_credentials_download_url does.
Apply:
@@ except Exception as e: log.error("Failed to retrieve infrastructure environments: %s", e) - return f"Error retrieving ISO URLs: {str(e)}" + return json.dumps({"error": f"Error retrieving ISO URLs: {str(e)}"}) @@ if not infra_envs: log.info("No infrastructure environments found for cluster %s", cluster_id) - return "No ISO download URLs found for this cluster." + return json.dumps({"error": "No ISO download URLs found for this cluster."}) @@ if not iso_info: log.info( "No ISO download URLs found in infrastructure environments for cluster %s", cluster_id, ) - return "No ISO download URLs found for this cluster." + return json.dumps({"error": "No ISO download URLs found for this cluster."})Also applies to: 56-59, 87-95
assisted_service_mcp/src/tools/version_tools.py (1)
37-45: Fix doc/behavior mismatch and ensure JSON-serializable payloadReturn the full list (not only latest) and convert models to dicts before dumping.
- try: - result = await client.get_openshift_versions(True) - log.info("Successfully retrieved OpenShift versions") - return json.dumps(result) + try: + result = await client.get_openshift_versions(False) + log.info("Successfully retrieved OpenShift versions") + payload = result.to_dict() if hasattr(result, "to_dict") else result + return json.dumps(payload)assisted_service_mcp/src/tools/network_tools.py (1)
211-221: Prevent double-encoded JSON for static_network_configAPI returns static_network_config as a JSON string in responses; json.dumps wraps it again. Handle str/None/list robustly.
- return json.dumps(infra_envs[0].get("static_network_config", [])) + value = infra_envs[0].get("static_network_config") + if value is None: + return "[]" + if isinstance(value, str): + return value # already a JSON string from API + if isinstance(value, list): + return json.dumps(value) + log.warning("Unexpected type for static_network_config: %s", type(value).__name__) + return "[]"Based on learnings
🧹 Nitpick comments (10)
pyrightconfig.json (2)
3-10: Drop redundant “ignore” for a path already in “exclude”.integration_test/performance is both excluded and ignored. Keep one. If you don’t want it analyzed at all, “exclude” is sufficient; remove the “ignore” block to reduce confusion.
"exclude": [ "integration_test/performance", ".venv", "venv" ], - "ignore": [ - "integration_test/performance" - ],
1-13: Optional: tighten Pyright config and reduce noise.
- Consider setting typeCheckingMode and pythonVersion explicitly.
- Point Pyright at your virtualenv for better lib resolution.
- excludeTests defaults to false; you can omit it.
Example:
{ "$schema": "https://raw.githubusercontent.com/microsoft/pyright/main/packages/pyright/schema/pyrightconfig.schema.json", + "typeCheckingMode": "standard", + "pythonVersion": "3.11", + "venvPath": ".", + "venv": ".venv", "exclude": [ "integration_test/performance", ".venv", "venv" ], - "ignore": [ - "integration_test/performance" - ], - "excludeTests": false, - "useLibraryCodeForTypes": false + "useLibraryCodeForTypes": true }Adjust pythonVersion/venv to your actual runtime.
pyproject.toml (1)
64-64: Scope “import-outside-toplevel” disable more narrowlyDisabling globally can hide real issues in app code. Prefer per-file disables (tests) or a tests-only rcfile override.
assisted_service_mcp/utils/helpers.py (1)
11-36: LGTM!The function correctly formats presigned URLs and handles the optional expiration timestamp. The use of
ZERO_DATETIMEas a sentinel value to distinguish meaningful expiration times is appropriate.The
.replace("+00:00", "Z")pattern on line 32-33 is common but could be made more robust:- presigned_url_dict["expires_at"] = presigned_url.expires_at.isoformat().replace( - "+00:00", "Z" - ) + # Format with 'Z' suffix for UTC instead of '+00:00' + iso_str = presigned_url.expires_at.isoformat() + presigned_url_dict["expires_at"] = iso_str[:-6] + "Z" if iso_str.endswith("+00:00") else iso_strHowever, the current implementation is widely used and unlikely to cause issues in practice.
assisted_service_mcp/src/tools/event_tools.py (1)
56-112: LGTM!The
host_eventsfunction is correctly implemented with proper documentation and error handling.For consistency with
cluster_events(line 47), consider creating the client on a separate line:try: log.info("Retrieving events for host %s in cluster %s", host_id, cluster_id) - client = InventoryClient(get_access_token_func()) + access_token = get_access_token_func() + client = InventoryClient(access_token) result = await client.get_events(cluster_id=cluster_id, host_id=host_id)This minor change would align the coding style between the two functions, though the current implementation is functionally correct.
assisted_service_mcp/src/tools/host_tools.py (1)
37-42: Clarify examples reflect MCP-exposed signature.Add a short note that auth is injected by the MCP wrapper; direct Python calls require get_access_token_func as the first arg.
Examples: + Note: When invoked as an MCP tool, authentication is injected. When calling this + function directly in Python, pass get_access_token_func as the first argument. - set_host_role("host-uuid", "cluster-uuid", "master") # Make this host a control plane nodetests/test_api.py (2)
11-13: Use MonkeyPatch.context() classmethod (or the monkeypatch fixture).The idiomatic pattern avoids creating an unused instance.
- with pytest.MonkeyPatch().context() as mp: + with pytest.MonkeyPatch.context() as mp:
35-37: Import metrics endpoint explicitly from the module (or confirm re-export).Avoid relying on package-level re-exports.
- from assisted_service_mcp.src.metrics import ( - metrics as metrics_endpoint, - ) # pylint: disable=import-outside-toplevel + from assisted_service_mcp.src.metrics.metrics import ( + metrics as metrics_endpoint, + ) # pylint: disable=import-outside-toplevelIf you intentionally re-export in metrics/init.py, keep as-is and ensure tests cover that.
tests/test_tools_module.py (1)
8-18: DRY: factor AssistedServiceMCPServer() into a fixture.Define a module-scoped fixture to construct the server once and inject it into tests to reduce repetition and speed up the suite.
If desired, I can provide a concrete fixture + test rewrites.
Also applies to: 45-47, 67-69, 91-96, 114-116, 137-141, 155-160, 172-176, 206-211, 291-297, 314-318, 326-334, 381-384, 394-400, 422-427, 455-461, 496-505
assisted_service_mcp/src/logger.py (1)
86-90: Make LOGGING_LEVEL handling robust; silence intentional local importSupport both string and integer levels and avoid linter noise.
- from assisted_service_mcp.src.settings import settings - - level = settings.LOGGING_LEVEL - return getattr(logging, str(level).upper(), logging.INFO) if level else logging.INFO + from assisted_service_mcp.src.settings import settings # pylint: disable=import-outside-toplevel + + level = getattr(settings, "LOGGING_LEVEL", None) + if isinstance(level, int): + return level + if isinstance(level, str) and level: + return getattr(logging, level.upper(), logging.INFO) + return logging.INFO
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (43)
Dockerfile(2 hunks)Makefile(2 hunks)README.md(2 hunks)assisted_service_mcp/__init__.py(1 hunks)assisted_service_mcp/src/__init__.py(1 hunks)assisted_service_mcp/src/api.py(1 hunks)assisted_service_mcp/src/logger.py(1 hunks)assisted_service_mcp/src/main.py(1 hunks)assisted_service_mcp/src/mcp.py(1 hunks)assisted_service_mcp/src/service_client/__init__.py(1 hunks)assisted_service_mcp/src/service_client/assisted_service_api.py(3 hunks)assisted_service_mcp/src/service_client/exceptions.py(1 hunks)assisted_service_mcp/src/service_client/helpers.py(1 hunks)assisted_service_mcp/src/settings.py(1 hunks)assisted_service_mcp/src/tools/__init__.py(1 hunks)assisted_service_mcp/src/tools/cluster_tools.py(1 hunks)assisted_service_mcp/src/tools/download_tools.py(1 hunks)assisted_service_mcp/src/tools/event_tools.py(1 hunks)assisted_service_mcp/src/tools/host_tools.py(1 hunks)assisted_service_mcp/src/tools/network_tools.py(1 hunks)assisted_service_mcp/src/tools/shared_helpers.py(1 hunks)assisted_service_mcp/src/tools/version_tools.py(1 hunks)assisted_service_mcp/src/utils/static_net/config.py(2 hunks)assisted_service_mcp/src/utils/static_net/template.py(5 hunks)assisted_service_mcp/utils/__init__.py(1 hunks)assisted_service_mcp/utils/auth.py(1 hunks)assisted_service_mcp/utils/helpers.py(1 hunks)integration_test/performance/README.md(1 hunks)pyproject.toml(4 hunks)pyrightconfig.json(1 hunks)server.py(0 hunks)tests/test_api.py(1 hunks)tests/test_assisted_service_api.py(4 hunks)tests/test_auth.py(1 hunks)tests/test_helpers.py(1 hunks)tests/test_logger_filter.py(1 hunks)tests/test_mcp.py(1 hunks)tests/test_metrics.py(1 hunks)tests/test_server.py(0 hunks)tests/test_settings.py(1 hunks)tests/test_shared_helpers.py(1 hunks)tests/test_static_net.py(3 hunks)tests/test_tools_module.py(1 hunks)
💤 Files with no reviewable changes (2)
- tests/test_server.py
- server.py
✅ Files skipped from review due to trivial changes (4)
- integration_test/performance/README.md
- assisted_service_mcp/src/tools/init.py
- assisted_service_mcp/utils/init.py
- README.md
🚧 Files skipped from review as they are similar to previous changes (17)
- assisted_service_mcp/src/service_client/helpers.py
- tests/test_logger_filter.py
- Dockerfile
- tests/test_helpers.py
- assisted_service_mcp/src/api.py
- assisted_service_mcp/src/main.py
- assisted_service_mcp/src/utils/static_net/template.py
- assisted_service_mcp/src/service_client/init.py
- tests/test_assisted_service_api.py
- tests/test_settings.py
- assisted_service_mcp/init.py
- tests/test_auth.py
- assisted_service_mcp/utils/auth.py
- tests/test_mcp.py
- Makefile
- assisted_service_mcp/src/init.py
- assisted_service_mcp/src/tools/cluster_tools.py
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: carbonin
PR: openshift-assisted/assisted-service-mcp#111
File: pyproject.toml:9-9
Timestamp: 2025-09-25T19:01:36.933Z
Learning: The `mcp` Python package (mcp>=1.15.0) includes FastMCP functionality and provides the same import path `from mcp.server.fastmcp import FastMCP` for backward compatibility with the standalone `fastmcp` package. This allows drop-in replacement when migrating from `fastmcp>=2.8.0` to `mcp>=1.15.0` without requiring code changes.
📚 Learning: 2025-09-09T18:51:46.598Z
Learnt from: keitwb
PR: openshift-assisted/assisted-service-mcp#91
File: service_client/static_net.py:21-36
Timestamp: 2025-09-09T18:51:46.598Z
Learning: In the assisted-service API, the static_network_config field is typed as a list when input to the API but comes back out as a string in responses. Functions processing this field from API responses should handle string inputs only.
Applied to files:
assisted_service_mcp/src/tools/network_tools.py
🧬 Code graph analysis (13)
tests/test_static_net.py (2)
assisted_service_mcp/src/utils/static_net/config.py (3)
remove_static_host_config_by_index(23-38)add_or_replace_static_host_config_yaml(41-70)validate_and_parse_nmstate(96-108)assisted_service_mcp/src/utils/static_net/template.py (2)
generate_nmstate_from_template(121-124)NMStateTemplateParams(102-118)
tests/test_tools_module.py (8)
assisted_service_mcp/src/mcp.py (1)
AssistedServiceMCPServer(26-163)assisted_service_mcp/src/service_client/assisted_service_api.py (14)
update_cluster(330-368)create_cluster(239-278)install_cluster(371-386)get_events(156-197)list_infra_envs(218-236)get_infra_env_download_url(516-540)get_presigned_for_cluster_credentials(484-513)update_host(453-481)add_operator_bundle_to_cluster(423-450)get_operator_bundles(410-420)get_infra_env(200-215)get_openshift_versions(389-407)create_infra_env(281-303)update_infra_env(306-327)assisted_service_mcp/src/tools/cluster_tools.py (5)
set_cluster_platform(290-330)set_cluster_vips(232-286)create_cluster(104-228)install_cluster(334-372)set_cluster_ssh_key(376-439)assisted_service_mcp/src/tools/event_tools.py (2)
cluster_events(12-53)host_events(57-112)assisted_service_mcp/src/tools/download_tools.py (2)
cluster_iso_download_url(14-95)cluster_credentials_download_url(99-167)assisted_service_mcp/src/tools/host_tools.py (1)
set_host_role(13-65)assisted_service_mcp/src/tools/version_tools.py (3)
add_operator_bundle_to_cluster(86-141)list_operator_bundles(49-82)list_versions(13-45)assisted_service_mcp/src/tools/network_tools.py (4)
validate_nmstate_yaml(22-55)alter_static_network_config_nmstate_for_host(106-175)list_static_network_config(179-221)generate_nmstate_yaml(59-102)
assisted_service_mcp/src/service_client/assisted_service_api.py (3)
assisted_service_mcp/src/settings.py (1)
get_setting(182-191)assisted_service_mcp/src/service_client/exceptions.py (1)
sanitize_exceptions(24-58)assisted_service_mcp/src/service_client/helpers.py (1)
Helpers(7-38)
assisted_service_mcp/src/tools/shared_helpers.py (2)
assisted_service_mcp/src/service_client/assisted_service_api.py (2)
InventoryClient(26-540)list_infra_envs(218-236)tests/test_assisted_service_api.py (1)
client(32-37)
assisted_service_mcp/src/tools/network_tools.py (5)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (4)
InventoryClient(26-540)get_infra_env(200-215)update_infra_env(306-327)list_infra_envs(218-236)assisted_service_mcp/src/utils/static_net/template.py (2)
NMStateTemplateParams(102-118)generate_nmstate_from_template(121-124)assisted_service_mcp/src/utils/static_net/config.py (3)
add_or_replace_static_host_config_yaml(41-70)remove_static_host_config_by_index(23-38)validate_and_parse_nmstate(96-108)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
assisted_service_mcp/src/tools/version_tools.py (2)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (3)
get_openshift_versions(389-407)get_operator_bundles(410-420)add_operator_bundle_to_cluster(423-450)
assisted_service_mcp/src/mcp.py (2)
assisted_service_mcp/utils/auth.py (2)
get_offline_token(10-45)get_access_token(48-114)assisted_service_mcp/src/utils/static_net/template.py (1)
NMStateTemplateParams(102-118)
assisted_service_mcp/src/tools/event_tools.py (2)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (1)
get_events(156-197)
assisted_service_mcp/src/tools/host_tools.py (3)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (2)
InventoryClient(26-540)update_host(453-481)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
assisted_service_mcp/src/tools/download_tools.py (3)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (3)
list_infra_envs(218-236)get_infra_env_download_url(516-540)get_presigned_for_cluster_credentials(484-513)assisted_service_mcp/utils/helpers.py (1)
format_presigned_url(11-36)
tests/test_metrics.py (1)
assisted_service_mcp/src/metrics/metrics.py (3)
metrics(69-71)initiate_metrics(44-48)track_tool_usage(51-65)
tests/test_shared_helpers.py (2)
assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)assisted_service_mcp/src/service_client/assisted_service_api.py (1)
list_infra_envs(218-236)
tests/test_api.py (1)
assisted_service_mcp/src/metrics/metrics.py (1)
metrics(69-71)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Red Hat Konflux / assisted-service-mcp-saas-main-on-pull-request
🔇 Additional comments (12)
pyproject.toml (2)
17-18: Good additions for centralized config; confirm dotenv is truly needed at runtimepydantic-settings belongs in main deps. If python-dotenv is only for local dev/.env loading, consider moving it to dev; keep in main only if production boot relies on it.
53-53: Verify path name for integration testsEnsure the ignored path matches your actual folder (e.g., integration_tests vs integration_test). Otherwise pylint won’t ignore as intended.
assisted_service_mcp/src/service_client/exceptions.py (1)
12-12: LGTM!The import path update correctly references the centralized logger module, aligning with the new modular architecture.
tests/test_metrics.py (1)
9-15: LGTM!The test correctly verifies that the
/metricsendpoint returns a 200 status code and includes Prometheus HELP text in the response.assisted_service_mcp/src/tools/event_tools.py (1)
12-53: LGTM!The
cluster_eventsfunction is well-structured with clear documentation, proper error handling, and appropriate logging.tests/test_shared_helpers.py (1)
8-28: LGTM!The tests provide good coverage of the
_get_cluster_infra_env_idhelper function:
- Success case with valid InfraEnv
- Error case when no InfraEnvs exist
- Error case when InfraEnv lacks a valid ID
The use of
AsyncMockis appropriate for testing async functions.tests/test_static_net.py (2)
12-20: LGTM!The import statements correctly reference the new modular structure under
assisted_service_mcp.src.utils.static_net.
84-98: LGTM!The test correctly expects
IndexErrorfor out-of-range operations, aligning with the changes inassisted_service_mcp/src/utils/static_net/config.pywhereValueErrorwas changed toIndexErrorfor index validation failures.assisted_service_mcp/src/tools/shared_helpers.py (1)
7-41: LGTM!The
_get_cluster_infra_env_idhelper function is well-implemented with:
- Clear error messages when no InfraEnv is found or when IDs are missing
- Warning log when multiple InfraEnvs exist (with graceful fallback to first valid one)
- Appropriate info-level logging for normal operation
The function provides a clean abstraction for InfraEnv resolution used by multiple tools.
assisted_service_mcp/src/utils/static_net/config.py (2)
33-36: LGTM!Changing from
ValueErrortoIndexErrorfor out-of-range index operations is semantically correct and aligns with Python conventions.
76-85: LGTM!The validation improvements strengthen input checking:
- Line 76-77: Explicitly validates that the
interfaceskey exists in the nmstate YAML- Line 84: Requires both
mac-addressandnameto be present, ensuring only complete interface definitions are processedThese changes provide clearer error messages and catch malformed configurations earlier in the processing pipeline.
assisted_service_mcp/src/tools/host_tools.py (1)
6-6: No change required for the metrics import. Confirmed thatassisted_service_mcp/src/metrics/__init__.pyexplicitly re-exportstrack_tool_usage, making the current import valid.
2803649 to
12b9b3e
Compare
283bd45 to
6dcc6ee
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (5)
README.md (1)
46-46: Inconsistent module invocation with Dockerfile.The SSE startup command uses
uv run assisted_service_mcp.src.main, but the Dockerfile (line 25) correctly usesuv run python -m assisted_service_mcp.src.main. The README command will fail becauseuv runrequires explicitpython -mto execute a module by its qualified name.Apply this diff to align with the Dockerfile:
-`OFFLINE_TOKEN=<your token> uv run assisted_service_mcp.src.main` +`OFFLINE_TOKEN=<your token> uv run python -m assisted_service_mcp.src.main`tests/test_log_analyzer.py (1)
10-15: Good: kwargs unused warning is suppressed without altering signatureThe inline pylint disable on kwargs keeps the mock’s API shape and satisfies lint.
assisted_service_mcp/src/tools/shared_helpers.py (1)
29-41: Scan all InfraEnvs and return the first with a valid ID (not just index 0)Current code logs “using the first valid one” but only inspects infra_envs[0], raising even when later entries are valid.
Apply:
@@ - infra_env_id = infra_envs[0].get("id") - if not infra_env_id: - raise ValueError(f"No InfraEnv with valid ID found for cluster {cluster_id}") - - log.info("Using InfraEnv %s for cluster %s", infra_env_id, cluster_id) - return infra_env_id + for infra_env in infra_envs: + infra_env_id = infra_env.get("id") + if infra_env_id: + log.info("Using InfraEnv %s for cluster %s", infra_env_id, cluster_id) + return infra_env_id + + raise ValueError(f"No InfraEnv with valid ID found for cluster {cluster_id}")assisted_service_mcp/src/tools/network_tools.py (1)
194-204: Bug: double-encoding static_network_config and brittle single-InfraEnv assumption
- static_network_config from API is a JSON string; json.dumps wraps it again.
- Returning an error when infra_envs != 1 is unfriendly; prefer shared helper to pick a valid InfraEnv.
Apply minimal fix for encoding:
- if len(infra_envs) != 1: + if len(infra_envs) != 1: log.warning( "cluster %s has %d infra_envs, expected 1", cluster_id, len(infra_envs) ) return "ERROR: this cluster doesn't have exactly 1 infra env, cannot manage static network config" - return json.dumps(infra_envs[0].get("static_network_config", [])) + value = infra_envs[0].get("static_network_config") + if value is None: + return "[]" + if isinstance(value, str): + # API returns JSON string; return as-is + return value + if isinstance(value, list): + # Already parsed; serialize to JSON + return json.dumps(value) + log.warning( + "Unexpected type for static_network_config: %s", + type(value).__name__, + ) + return "[]"Optional improvement: use the shared resolver and fetch the InfraEnv before reading its field for consistency.
# Alternative approach: infra_env_id = await _get_cluster_infra_env_id(client, cluster_id) infra_env = await client.get_infra_env(infra_env_id) return infra_env.static_network_config or "[]"assisted_service_mcp/src/logger.py (1)
79-90: Make LOGGING_LEVEL handling robust (ints/strings) and silence intentional local importCurrent code degrades int levels to INFO and may trigger pylint C0415.
Apply:
- # Import here to avoid circular dependency at module load time - from assisted_service_mcp.src.settings import settings - - level = settings.LOGGING_LEVEL - return getattr(logging, str(level).upper(), logging.INFO) if level else logging.INFO + # Import here to avoid circular dependency at module load time + from assisted_service_mcp.src.settings import settings # pylint: disable=import-outside-toplevel + + level = getattr(settings, "LOGGING_LEVEL", None) + if isinstance(level, int): + return level + if isinstance(level, str) and level: + return getattr(logging, level.upper(), logging.INFO) + return logging.INFO
🧹 Nitpick comments (18)
assisted_service_mcp/utils/helpers.py (1)
11-36: Function logic is sound; consider more defensive timezone handling.The function correctly handles both
NoneandZERO_DATETIMEcases forexpires_at. The overall implementation is functional and appropriate for the use case.However, the
.replace("+00:00", "Z")pattern for ISO8601 formatting assumes UTC times. If the API ever returns a non-UTC datetime, this replacement will silently fail to format correctly. Consider a more defensive approach.Apply this diff for more robust timezone handling:
- if presigned_url.expires_at and presigned_url.expires_at != ZERO_DATETIME: - presigned_url_dict["expires_at"] = presigned_url.expires_at.isoformat().replace( - "+00:00", "Z" - ) + if presigned_url.expires_at and presigned_url.expires_at != ZERO_DATETIME: + # Ensure UTC and format with 'Z' suffix + expires_utc = presigned_url.expires_at.astimezone(timezone.utc) + presigned_url_dict["expires_at"] = expires_utc.isoformat().replace("+00:00", "Z")Optional nitpick: The raw string literal
r"""on line 12 is unnecessary since the docstring contains no backslashes.assisted_service_mcp/src/tools/operator_tools.py (1)
38-41: Prefer logging exceptions with stack tracesUse log.exception in except blocks to capture full context.
- except Exception as e: - log.error("Failed to retrieve operator bundles: %s", str(e)) - raise + except Exception: + log.exception("Failed to retrieve operator bundles") + raise @@ - except Exception as e: - log.error( - "Failed to add operator bundle '%s' to cluster %s: %s", - bundle_name, - cluster_id, - str(e), - ) - raise + except Exception: + log.exception( + "Failed to add operator bundle '%s' to cluster %s", bundle_name, cluster_id + ) + raiseAlso applies to: 90-96
assisted_service_mcp/src/service_client/exceptions.py (1)
12-12: Import path LGTM; optionally normalize ApiException body to str
- New logger import is correct given centralized logger.
- Optional: if e.body is bytes, decode before logging/including in message to avoid b'...'.
Apply this diff to make body handling robust:
- except ApiException as e: - log.error( - "API error during %s: Status: %s, Reason: %s, Body: %s", - operation_name, - e.status, - e.reason, - e.body, - ) - - error_msg = f"API error: Status {e.status}" - if e.status and 400 <= e.status <= 499 and e.body: - error_msg += f", Details: {e.body}" + except ApiException as e: + body = ( + e.body.decode("utf-8", "replace") + if isinstance(e.body, (bytes, bytearray)) + else e.body + ) + log.error( + "API error during %s: Status: %s, Reason: %s, Body: %s", + operation_name, + e.status, + e.reason, + body, + ) + + error_msg = f"API error: Status {e.status}" + if e.status and 400 <= e.status <= 499 and body: + error_msg += f", Details: {body}"Please confirm
assisted_service_mcp.src.loggerexports a module-levellogat import time so imports in leaf modules never fail.Also applies to: 41-53
tests/test_logger.py (2)
9-15: Isolate env changes to avoid cross-test leakageUse a temporary env overlay when reloading settings to prevent persistent mutations across tests.
Apply this diff:
+from unittest.mock import patch @@ -def _reload_settings(env: dict[str, str]) -> None: # type: ignore[no-untyped-def] - os.environ.update(env) - mod = "assisted_service_mcp.src.settings" - if mod in sys.modules: - del sys.modules[mod] - importlib.import_module(mod) +def _reload_settings(env: dict[str, str]) -> None: # type: ignore[no-untyped-def] + mod = "assisted_service_mcp.src.settings" + with patch.dict(os.environ, env, clear=False): + if mod in sys.modules: + del sys.modules[mod] + importlib.import_module(mod)
17-35: Optionally assert SensitiveFormatter on handlersStrengthen tests by asserting handlers use SensitiveFormatter.
Add:
assert all(isinstance(h.formatter, SensitiveFormatter) for h in logger.handlers)in both configure_logging tests.
Also applies to: 37-53
assisted_service_mcp/src/tools/host_tools.py (1)
58-60: Consider returning structured data instead of stringReturning models.Host as dict (or a concise summary) can be more useful to clients than to_str().
For example:
return result.to_dict()tests/test_assisted_service_api.py (1)
61-71: Patching settings via singleton is fine; env overlay is an alternativeCurrent approach works. Alternatively, patch dict(os.environ) and reload settings to better simulate runtime config resolution.
tests/test_service_client_api.py (1)
45-56: Prefer pytest-asyncio over asyncio.run for consistencyMark the test with @pytest.mark.asyncio and await the call instead of using asyncio.run().
Example:
@pytest.mark.asyncio async def test_update_cluster_vips_and_platform_mapping() -> None: ... await client.update_cluster(...)tests/test_auth.py (2)
52-69: Patch the function where it’s used for stabilityPatch the requests call in the module under test instead of the global symbol to avoid leakage and future import aliasing issues.
Apply:
-@patch("requests.post") +@patch("assisted_service_mcp.utils.auth.requests.post") def test_get_access_token_via_offline_token(mock_post: Mock) -> None: # type: ignore[no-untyped-def]
28-38: Add edge-case tests for resilience
- OFFLINE_TOKEN="" (empty string) should fall back to header.
- Authorization header case-insensitivity (e.g., "bearer xyz") should be accepted.
I can add concise tests covering these cases if desired.
Also applies to: 47-50
assisted_service_mcp/utils/auth.py (1)
35-43: Defensive access to request_context/request to avoid AttributeErrorAvoid assuming context.request_context/request exist; use getattr to be safe with different MCP contexts/mocks.
Apply:
- context = mcp.get_context() - if context and context.request_context: - request = context.request_context.request + context = mcp.get_context() + request_context = getattr(context, "request_context", None) + if request_context: + request = getattr(request_context, "request", None) if request is not None: token = request.headers.get("OCM-Offline-Token")And:
- context = mcp.get_context() - if context and context.request_context: - request = context.request_context.request + context = mcp.get_context() + request_context = getattr(context, "request_context", None) + if request_context: + request = getattr(request_context, "request", None) if request is not None: header = request.headers.get("Authorization")Also applies to: 71-81
tests/test_tools_module.py (3)
24-24: Use a module-scoped fixture for AssistedServiceMCPServer to reduce overheadRepeated instantiation is unnecessary and slows tests. Create once per module.
Example:
@pytest.fixture(scope="module", autouse=True) def _server() -> None: from assisted_service_mcp.src.mcp import AssistedServiceMCPServer AssistedServiceMCPServer()Remove per-test AssistedServiceMCPServer() calls.
Also applies to: 51-51, 73-74, 97-98, 120-121
163-171: Honor the expires_at parameter in _Presigned helperThe current helper ignores the provided ISO timestamp. Parse the given value instead.
Apply:
class _Presigned: - def __init__(self, url: str, expires_at: str | None = None) -> None: - self.url = url - self.expires_at = ( - _dt.datetime.fromisoformat("2025-01-01T00:00:00+00:00") - if expires_at - else None - ) + def __init__(self, url: str, expires_at: str | None = None) -> None: + self.url = url + if expires_at: + # Support Z suffix + ts = expires_at.replace("Z", "+00:00") + try: + self.expires_at = _dt.datetime.fromisoformat(ts) + except ValueError: + self.expires_at = None + else: + self.expires_at = None
26-34: Drop redundant get_access_token patchingTool functions accept get_access_token_func and never call utils.auth.get_access_token directly. The patches add noise without effect.
Remove the with patch("assisted_service_mcp.utils.auth.get_access_token", ...) contexts in these tests; keep passing the lambda token provider.
Also applies to: 58-61, 104-107, 127-130, 185-187, 219-221, 251-253, 278-280
assisted_service_mcp/src/logger.py (3)
144-144: Silence C0415 for the intentional in-function importAvoid linter noise.
Apply:
- from assisted_service_mcp.src.settings import settings + from assisted_service_mcp.src.settings import settings # pylint: disable=import-outside-toplevel
146-153: Prevent duplicate logs by disabling propagationWithout propagate=False, messages may bubble to parent/root handlers and duplicate.
Apply:
logger_name = settings.LOGGER_NAME or "assisted-service-mcp" target_logger = logging.getLogger(logger_name) + target_logger.propagate = False @@ - urllib3_logger = logging.getLogger("urllib3") + urllib3_logger = logging.getLogger("urllib3") + urllib3_logger.propagate = FalseAlso applies to: 162-173
93-96: Optional: set third-party log levels in one placeYou set levels here and again in configure_logging(). Consider doing it only in configure_logging() to keep initialization idempotent.
Remove these top-level setLevel calls and keep them in configure_logging().
assisted_service_mcp/src/tools/cluster_tools.py (1)
7-12: Move helper import to top-level; avoid repeated in-function importNo circular dependency is evident; importing once improves clarity and avoids repeated import cost.
Apply:
from assisted_service_mcp.src.logger import log from assisted_service_mcp.src.utils.log_analyzer.main import analyze_cluster +from assisted_service_mcp.src.tools.shared_helpers import _get_cluster_infra_env_id @@ - # Import helper function here to avoid circular imports - from assisted_service_mcp.src.tools.shared_helpers import _get_cluster_infra_env_idAlso applies to: 389-391
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (51)
Dockerfile(2 hunks)Makefile(2 hunks)README.md(2 hunks)assisted_service_mcp/__init__.py(1 hunks)assisted_service_mcp/src/__init__.py(1 hunks)assisted_service_mcp/src/api.py(1 hunks)assisted_service_mcp/src/logger.py(1 hunks)assisted_service_mcp/src/main.py(1 hunks)assisted_service_mcp/src/mcp.py(1 hunks)assisted_service_mcp/src/service_client/__init__.py(1 hunks)assisted_service_mcp/src/service_client/assisted_service_api.py(3 hunks)assisted_service_mcp/src/service_client/exceptions.py(1 hunks)assisted_service_mcp/src/service_client/helpers.py(1 hunks)assisted_service_mcp/src/settings.py(1 hunks)assisted_service_mcp/src/tools/__init__.py(1 hunks)assisted_service_mcp/src/tools/cluster_tools.py(1 hunks)assisted_service_mcp/src/tools/download_tools.py(1 hunks)assisted_service_mcp/src/tools/event_tools.py(1 hunks)assisted_service_mcp/src/tools/host_tools.py(1 hunks)assisted_service_mcp/src/tools/network_tools.py(1 hunks)assisted_service_mcp/src/tools/operator_tools.py(1 hunks)assisted_service_mcp/src/tools/shared_helpers.py(1 hunks)assisted_service_mcp/src/tools/version_tools.py(1 hunks)assisted_service_mcp/src/utils/log_analyzer/main.py(1 hunks)assisted_service_mcp/src/utils/log_analyzer/signatures/advanced_analysis.py(1 hunks)assisted_service_mcp/src/utils/log_analyzer/signatures/error_detection.py(1 hunks)assisted_service_mcp/src/utils/log_analyzer/signatures/networking.py(1 hunks)assisted_service_mcp/src/utils/static_net/config.py(2 hunks)assisted_service_mcp/src/utils/static_net/template.py(5 hunks)assisted_service_mcp/utils/__init__.py(1 hunks)assisted_service_mcp/utils/auth.py(1 hunks)assisted_service_mcp/utils/helpers.py(1 hunks)integration_test/performance/README.md(1 hunks)pyproject.toml(4 hunks)pyrightconfig.json(1 hunks)server.py(0 hunks)service_client/logger.py(0 hunks)tests/test_api.py(1 hunks)tests/test_assisted_service_api.py(4 hunks)tests/test_auth.py(1 hunks)tests/test_helpers.py(1 hunks)tests/test_log_analyzer.py(1 hunks)tests/test_logger.py(1 hunks)tests/test_mcp.py(1 hunks)tests/test_metrics.py(1 hunks)tests/test_server.py(0 hunks)tests/test_service_client_api.py(1 hunks)tests/test_settings.py(1 hunks)tests/test_shared_helpers.py(1 hunks)tests/test_static_net.py(3 hunks)tests/test_tools_module.py(1 hunks)
💤 Files with no reviewable changes (3)
- server.py
- service_client/logger.py
- tests/test_server.py
✅ Files skipped from review due to trivial changes (2)
- tests/test_shared_helpers.py
- assisted_service_mcp/src/utils/log_analyzer/signatures/advanced_analysis.py
🚧 Files skipped from review as they are similar to previous changes (24)
- assisted_service_mcp/src/service_client/init.py
- assisted_service_mcp/src/service_client/assisted_service_api.py
- assisted_service_mcp/src/settings.py
- assisted_service_mcp/src/main.py
- assisted_service_mcp/src/init.py
- pyrightconfig.json
- assisted_service_mcp/src/utils/log_analyzer/main.py
- assisted_service_mcp/src/utils/static_net/config.py
- assisted_service_mcp/src/utils/log_analyzer/signatures/error_detection.py
- tests/test_metrics.py
- assisted_service_mcp/src/api.py
- tests/test_mcp.py
- integration_test/performance/README.md
- assisted_service_mcp/src/tools/event_tools.py
- tests/test_api.py
- assisted_service_mcp/src/service_client/helpers.py
- tests/test_helpers.py
- assisted_service_mcp/src/tools/init.py
- tests/test_static_net.py
- tests/test_settings.py
- assisted_service_mcp/src/tools/version_tools.py
- assisted_service_mcp/src/mcp.py
- assisted_service_mcp/src/tools/download_tools.py
- assisted_service_mcp/utils/init.py
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: carbonin
PR: openshift-assisted/assisted-service-mcp#111
File: pyproject.toml:9-9
Timestamp: 2025-09-25T19:01:36.933Z
Learning: The `mcp` Python package (mcp>=1.15.0) includes FastMCP functionality and provides the same import path `from mcp.server.fastmcp import FastMCP` for backward compatibility with the standalone `fastmcp` package. This allows drop-in replacement when migrating from `fastmcp>=2.8.0` to `mcp>=1.15.0` without requiring code changes.
📚 Learning: 2025-09-09T18:51:46.598Z
Learnt from: keitwb
PR: openshift-assisted/assisted-service-mcp#91
File: service_client/static_net.py:21-36
Timestamp: 2025-09-09T18:51:46.598Z
Learning: In the assisted-service API, the static_network_config field is typed as a list when input to the API but comes back out as a string in responses. Functions processing this field from API responses should handle string inputs only.
Applied to files:
assisted_service_mcp/src/tools/network_tools.py
🧬 Code graph analysis (12)
assisted_service_mcp/src/tools/host_tools.py (3)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (2)
InventoryClient(27-557)update_host(470-498)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
tests/test_auth.py (1)
assisted_service_mcp/utils/auth.py (2)
get_offline_token(10-45)get_access_token(48-114)
assisted_service_mcp/src/tools/operator_tools.py (3)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (3)
InventoryClient(27-557)get_operator_bundles(427-437)add_operator_bundle_to_cluster(440-467)tests/test_tools_module.py (2)
to_str(450-451)to_str(494-495)
tests/test_logger.py (1)
assisted_service_mcp/src/logger.py (3)
SensitiveFormatter(15-76)configure_logging(132-178)_filter(28-63)
assisted_service_mcp/utils/auth.py (2)
assisted_service_mcp/src/settings.py (1)
get_setting(193-202)tests/test_auth.py (1)
get_context(24-25)
tests/test_service_client_api.py (1)
assisted_service_mcp/src/service_client/assisted_service_api.py (3)
InventoryClient(27-557)_get_host(109-114)update_cluster(347-385)
tests/test_assisted_service_api.py (2)
assisted_service_mcp/src/service_client/assisted_service_api.py (1)
InventoryClient(27-557)assisted_service_mcp/src/service_client/exceptions.py (1)
AssistedServiceAPIError(15-16)
tests/test_log_analyzer.py (5)
assisted_service_mcp/src/utils/log_analyzer/log_analyzer.py (5)
LogAnalyzer(22-228)metadata(39-53)get_last_install_cluster_events(77-88)get_events_by_host(126-132)get_host_log_file(134-165)assisted_service_mcp/src/utils/log_analyzer/main.py (1)
analyze_cluster(14-71)assisted_service_mcp/src/utils/log_analyzer/signatures/basic_info.py (1)
ComponentsVersionSignature(14-55)assisted_service_mcp/src/utils/log_analyzer/signatures/error_detection.py (8)
analyze(39-62)analyze(72-90)analyze(98-118)analyze(126-150)analyze(158-185)analyze(191-213)analyze(229-260)SNOHostnameHasEtcd(36-62)assisted_service_mcp/src/utils/log_analyzer/signatures/networking.py (4)
analyze(30-71)analyze(78-178)analyze(220-248)SNOMachineCidrSignature(27-71)
tests/test_tools_module.py (4)
assisted_service_mcp/src/tools/version_tools.py (1)
list_versions(12-38)assisted_service_mcp/src/tools/operator_tools.py (2)
list_operator_bundles(13-40)add_operator_bundle_to_cluster(44-96)assisted_service_mcp/src/service_client/assisted_service_api.py (11)
add_operator_bundle_to_cluster(440-467)update_cluster(347-385)install_cluster(388-403)list_infra_envs(235-253)get_infra_env_download_url(533-557)get_presigned_for_cluster_credentials(501-530)update_host(470-498)get_operator_bundles(427-437)get_infra_env(217-232)get_openshift_versions(406-424)update_infra_env(323-344)assisted_service_mcp/src/mcp.py (1)
AssistedServiceMCPServer(27-168)
assisted_service_mcp/src/tools/cluster_tools.py (5)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (8)
InventoryClient(27-557)get_cluster(117-141)list_clusters(144-154)create_cluster(256-295)create_infra_env(298-320)update_cluster(347-385)install_cluster(388-403)update_infra_env(323-344)assisted_service_mcp/src/service_client/helpers.py (1)
Helpers(7-38)assisted_service_mcp/src/utils/log_analyzer/main.py (1)
analyze_cluster(14-71)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
assisted_service_mcp/src/tools/network_tools.py (5)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (4)
InventoryClient(27-557)get_infra_env(217-232)update_infra_env(323-344)list_infra_envs(235-253)assisted_service_mcp/src/utils/static_net/template.py (2)
NMStateTemplateParams(102-118)generate_nmstate_from_template(121-124)assisted_service_mcp/src/utils/static_net/config.py (3)
add_or_replace_static_host_config_yaml(41-70)remove_static_host_config_by_index(23-38)validate_and_parse_nmstate(96-108)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
assisted_service_mcp/src/tools/shared_helpers.py (1)
assisted_service_mcp/src/service_client/assisted_service_api.py (2)
InventoryClient(27-557)list_infra_envs(235-253)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Red Hat Konflux / assisted-service-mcp-saas-main-on-pull-request
🔇 Additional comments (17)
assisted_service_mcp/utils/helpers.py (1)
7-8: LGTM!The ZERO_DATETIME constant is well-defined and clearly serves as a sentinel value to detect placeholder dates.
assisted_service_mcp/__init__.py (1)
1-6: LGTM!The package initialization is clean and follows best practices with a descriptive docstring and semantic versioning.
README.md (1)
32-32: LGTM!The VSCode configuration correctly references the new module entry point file path.
Dockerfile (2)
14-14: LGTM!Consolidating multiple COPY directives into a single command improves maintainability and Docker build layer efficiency.
25-25: LGTM!The CMD correctly uses
python -mto run the module entry point, following Python best practices for package execution.Makefile (2)
18-18: LGTM!The run-local target correctly uses
python -mfor module invocation, consistent with the Dockerfile approach.
33-33: LGTM!Using
--cov=assisted_service_mcpis the right approach. It covers the entire package including all the newly relocated modules (service_client, static_net, metrics, etc.) under assisted_service_mcp/src/, making it simpler and more comprehensive than targeting individual modules.assisted_service_mcp/src/utils/static_net/template.py (5)
14-16: LGTM: Field formatting improved for consistency.The two-line Field declaration with explicit description parameter improves readability and aligns with the coding style used throughout the file.
218-218: LGTM: More idiomatic YAML formatting.Removing the trailing space after
port:produces more idiomatic YAML. Both formats are valid, but this is cleaner.
66-68: Tests cover valid two-port usage—no breaking changes. All existing instantiations use at least two interfaces; merging is safe.
85-85: VLAN ID validation is correct; existing tests use IDs (100, 200) within 1–4094; no breaking changes detected.
124-124: Rendering change is covered by existing tests.Tests in tests/test_static_net.py verify various NMStateTemplateParams scenarios and validate the generated YAML.
assisted_service_mcp/src/utils/log_analyzer/signatures/networking.py (1)
13-17: Imports and symbols validated NEW_LOG_BUNDLE_PATH, OLD_LOG_BUNDLE_PATH, operator_statuses_from_controller_logs, and filter_operators are correctly defined in their respective modules.assisted_service_mcp/src/tools/host_tools.py (1)
12-28: Auth injection param: verify it is hidden from tool schemaImplementation looks good. Ensure registration wraps this function so
get_access_token_funcis not exposed as a tool argument to clients.tests/test_assisted_service_api.py (2)
12-13: Import path updates align with new layoutNew imports from assisted_service_mcp.src.service_client.* look correct.
141-141: ApiClient patch target is correctPatching ApiClient in the module under test ensures the constructor interception works.
tests/test_service_client_api.py (1)
13-21: Patch targets look correctPatching get_setting and Configuration at the module-under-test level is appropriate and keeps the test hermetic.
6dcc6ee to
227b497
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (7)
assisted_service_mcp/src/utils/static_net/config.py (1)
74-77: Critical: Guard against None/non-dict nmstate to prevent AttributeError.The issue flagged in the previous review remains unresolved.
validate_and_parse_nmstate(nmstate_yaml)returnsNonefor empty strings or YAML null values (viayaml.safe_load). Line 75 calls.get("interfaces")on that result, which raisesAttributeErrorwhennmstateisNoneor any non-dict type.Apply this diff to guard before accessing:
def _generate_host_static_config(nmstate_yaml: str) -> HostStaticNetworkConfig: nmstate = validate_and_parse_nmstate(nmstate_yaml) + if not isinstance(nmstate, dict): + raise ValueError("nmstate YAML must be a mapping with an 'interfaces' key") interfaces = nmstate.get("interfaces") if interfaces is None: raise ValueError("nmstate YAML must contain an 'interfaces' key")tests/test_api.py (1)
39-44: Route detection is correct and non-brittle.Using existing_paths via r.path avoids false negatives/duplicates. Good fix.
assisted_service_mcp/src/tools/version_tools.py (1)
30-38: Fix runtime serialization bug and doc/behavior mismatch.
- json.dumps(result) will fail if result is a model (not JSON-serializable).
- Passing True returns only latest versions, but docstring promises full list.
Apply:
- try: - result = await client.get_openshift_versions(True) - log.info("Successfully retrieved OpenShift versions") - return json.dumps(result) + try: + result = await client.get_openshift_versions(False) + log.info("Successfully retrieved OpenShift versions") + payload = result.to_dict() if hasattr(result, "to_dict") else result + return json.dumps(payload)assisted_service_mcp/src/logger.py (1)
86-90: Harden LOGGING_LEVEL handling; silence intentional in-function imports; avoid duplicate propagation.
- Support int levels and invalid strings; add pylint disable for import-outside-toplevel.
- Set propagate=False to avoid double logging when root has handlers.
- # Import here to avoid circular dependency at module load time - from assisted_service_mcp.src.settings import settings - - level = settings.LOGGING_LEVEL - return getattr(logging, str(level).upper(), logging.INFO) if level else logging.INFO + # Import here to avoid circular dependency at module load time + from assisted_service_mcp.src.settings import settings # pylint: disable=import-outside-toplevel + + level = getattr(settings, "LOGGING_LEVEL", None) + if isinstance(level, int): + return level + if isinstance(level, str) and level: + return getattr(logging, level.upper(), logging.INFO) + return logging.INFO- # Import inside function to avoid circular dependency - from assisted_service_mcp.src.settings import settings + # Import inside function to avoid circular dependency + from assisted_service_mcp.src.settings import settings # pylint: disable=import-outside-toplevel @@ - target_logger = logging.getLogger(logger_name) + target_logger = logging.getLogger(logger_name) + target_logger.propagate = FalseAlso applies to: 144-149
assisted_service_mcp/src/tools/download_tools.py (1)
43-91: Inconsistent return format breaks JSON parsing.This function returns plain strings for errors (lines 50, 54, 88) but JSON for success (line 91), making reliable client-side parsing impossible.
assisted_service_mcp/src/tools/network_tools.py (1)
204-204: Double-encoded JSON returned for static_network_config.When the API returns a JSON string,
json.dumpswraps it again producing a JSON string literal, not an array.Based on learnings
assisted_service_mcp/src/tools/cluster_tools.py (1)
389-390: Move import to top-level.The local import of
_get_cluster_infra_env_idshould be relocated to the module's top-level imports. Past verification confirmed no circular dependency exists betweencluster_toolsandshared_helpers.
🧹 Nitpick comments (9)
Dockerfile (1)
14-14: Harden build reproducibility and slim runtime deps.
- Keep as-is functionally, but prefer uv sync with lock enforcement and no dev deps for the container image.
You can adjust outside these lines:
# Prefer locked/prod deps only RUN uv sync --frozen --no-devAlso applies to: 25-25
tests/test_mcp.py (1)
8-11: Strengthen assertion: verify closures are callable.Also assert callable(getattr(server, "_get_offline_token")) and callable(getattr(server, "_get_access_token")) to catch non-callable attributes.
tests/test_api.py (1)
8-18: Optional: clear import caches before re-importing.Call importlib.invalidate_caches() prior to deleting/importing modules to avoid any stale path caching on some importers.
importlib.invalidate_caches() if "assisted_service_mcp.src.api" in sys.modules: del sys.modules["assisted_service_mcp.src.api"] api_mod = importlib.import_module("assisted_service_mcp.src.api")assisted_service_mcp/src/tools/operator_tools.py (1)
88-88: Unify tool outputs: return JSON instead of stringified model.For consistency with list_operator_bundles and other tools, return JSON (dict) representation.
- return result.to_str() + return json.dumps(result.to_dict() if hasattr(result, "to_dict") else result)assisted_service_mcp/src/tools/event_tools.py (1)
46-48: Preserve stack traces on failures (use log.exception).Current error logs drop traceback. Use log.exception (or exc_info=True) to aid debugging.
- except Exception as e: - log.error("Failed to retrieve events for cluster %s: %s", cluster_id, str(e)) - raise + except Exception: + log.exception("Failed to retrieve events for cluster %s", cluster_id) + raise- except Exception as e: - log.error( - "Failed to retrieve events for host %s in cluster %s: %s", - host_id, - cluster_id, - str(e), - ) - raise + except Exception: + log.exception( + "Failed to retrieve events for host %s in cluster %s", + host_id, + cluster_id, + ) + raiseAlso applies to: 95-102
assisted_service_mcp/src/settings.py (2)
189-191: Fix misleading comment: Settings() does validate on instantiation.Pydantic validates at creation time; extra checks happen in main via validate_config.
-# Create config instance without validation (validation happens in main.py if needed) +# Create config instance (Pydantic validates on instantiation; additional checks may run in main.py) settings = Settings()
137-146: Prefer boolean for feature toggles.Using int 0/1 works but a bool is clearer; pydantic parses "0/1/true/false" into bool.
- ENABLE_TROUBLESHOOTING_TOOLS: int = Field( - default=0, - ge=0, - le=1, + ENABLE_TROUBLESHOOTING_TOOLS: bool = Field( + default=False, json_schema_extra={ "env": "ENABLE_TROUBLESHOOTING_TOOLS", "description": "Whether the troubleshooting tool call(s) should be enabled", - "example": 0, + "example": False, }, )tests/test_settings.py (2)
10-16: Streamline environment patching and re-import for isolation.Use the classmethod context() and reload via sys.modules pop to avoid double reloads and an extra unused instance.
- with pytest.MonkeyPatch().context() as mp: + with pytest.MonkeyPatch.context() as mp: for k, v in env.items(): mp.setenv(k, v) - # Re-import to apply env overrides - settings_mod = importlib.import_module(module_name) - importlib.reload(settings_mod) + # Re-import to apply env overrides + sys.modules.pop(module_name, None) + settings_mod = importlib.import_module(module_name) return settings_mod.settings
47-54: Deduplicate invalid TRANSPORT tests.Both tests assert invalid TRANSPORT raises; keep the ValidationError-focused one and drop the generic duplicate, or parametrize.
Also applies to: 71-73
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (51)
Dockerfile(2 hunks)Makefile(2 hunks)README.md(2 hunks)assisted_service_mcp/__init__.py(1 hunks)assisted_service_mcp/src/__init__.py(1 hunks)assisted_service_mcp/src/api.py(1 hunks)assisted_service_mcp/src/logger.py(1 hunks)assisted_service_mcp/src/main.py(1 hunks)assisted_service_mcp/src/mcp.py(1 hunks)assisted_service_mcp/src/service_client/__init__.py(1 hunks)assisted_service_mcp/src/service_client/assisted_service_api.py(3 hunks)assisted_service_mcp/src/service_client/exceptions.py(1 hunks)assisted_service_mcp/src/service_client/helpers.py(1 hunks)assisted_service_mcp/src/settings.py(1 hunks)assisted_service_mcp/src/tools/__init__.py(1 hunks)assisted_service_mcp/src/tools/cluster_tools.py(1 hunks)assisted_service_mcp/src/tools/download_tools.py(1 hunks)assisted_service_mcp/src/tools/event_tools.py(1 hunks)assisted_service_mcp/src/tools/host_tools.py(1 hunks)assisted_service_mcp/src/tools/network_tools.py(1 hunks)assisted_service_mcp/src/tools/operator_tools.py(1 hunks)assisted_service_mcp/src/tools/shared_helpers.py(1 hunks)assisted_service_mcp/src/tools/version_tools.py(1 hunks)assisted_service_mcp/src/utils/log_analyzer/main.py(1 hunks)assisted_service_mcp/src/utils/log_analyzer/signatures/advanced_analysis.py(1 hunks)assisted_service_mcp/src/utils/log_analyzer/signatures/error_detection.py(1 hunks)assisted_service_mcp/src/utils/log_analyzer/signatures/networking.py(1 hunks)assisted_service_mcp/src/utils/static_net/config.py(2 hunks)assisted_service_mcp/src/utils/static_net/template.py(5 hunks)assisted_service_mcp/utils/__init__.py(1 hunks)assisted_service_mcp/utils/auth.py(1 hunks)assisted_service_mcp/utils/helpers.py(1 hunks)integration_test/performance/README.md(1 hunks)pyproject.toml(4 hunks)pyrightconfig.json(1 hunks)server.py(0 hunks)service_client/logger.py(0 hunks)tests/test_api.py(1 hunks)tests/test_assisted_service_api.py(4 hunks)tests/test_auth.py(1 hunks)tests/test_helpers.py(1 hunks)tests/test_log_analyzer.py(1 hunks)tests/test_logger.py(1 hunks)tests/test_mcp.py(1 hunks)tests/test_metrics.py(1 hunks)tests/test_server.py(0 hunks)tests/test_service_client_api.py(1 hunks)tests/test_settings.py(1 hunks)tests/test_shared_helpers.py(1 hunks)tests/test_static_net.py(3 hunks)tests/test_tools_module.py(1 hunks)
💤 Files with no reviewable changes (3)
- tests/test_server.py
- service_client/logger.py
- server.py
✅ Files skipped from review due to trivial changes (3)
- tests/test_tools_module.py
- assisted_service_mcp/utils/init.py
- assisted_service_mcp/src/init.py
🚧 Files skipped from review as they are similar to previous changes (17)
- assisted_service_mcp/src/tools/shared_helpers.py
- integration_test/performance/README.md
- tests/test_service_client_api.py
- assisted_service_mcp/src/api.py
- assisted_service_mcp/src/service_client/init.py
- Makefile
- tests/test_logger.py
- assisted_service_mcp/src/main.py
- assisted_service_mcp/init.py
- tests/test_static_net.py
- assisted_service_mcp/utils/helpers.py
- assisted_service_mcp/utils/auth.py
- assisted_service_mcp/src/service_client/assisted_service_api.py
- pyrightconfig.json
- tests/test_auth.py
- tests/test_shared_helpers.py
- tests/test_helpers.py
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: carbonin
PR: openshift-assisted/assisted-service-mcp#111
File: pyproject.toml:9-9
Timestamp: 2025-09-25T19:01:36.933Z
Learning: The `mcp` Python package (mcp>=1.15.0) includes FastMCP functionality and provides the same import path `from mcp.server.fastmcp import FastMCP` for backward compatibility with the standalone `fastmcp` package. This allows drop-in replacement when migrating from `fastmcp>=2.8.0` to `mcp>=1.15.0` without requiring code changes.
📚 Learning: 2025-09-09T18:51:46.598Z
Learnt from: keitwb
PR: openshift-assisted/assisted-service-mcp#91
File: service_client/static_net.py:21-36
Timestamp: 2025-09-09T18:51:46.598Z
Learning: In the assisted-service API, the static_network_config field is typed as a list when input to the API but comes back out as a string in responses. Functions processing this field from API responses should handle string inputs only.
Applied to files:
assisted_service_mcp/src/tools/network_tools.py
🧬 Code graph analysis (14)
assisted_service_mcp/src/tools/version_tools.py (2)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (2)
InventoryClient(27-557)get_openshift_versions(406-424)
tests/test_log_analyzer.py (5)
assisted_service_mcp/src/utils/log_analyzer/log_analyzer.py (5)
LogAnalyzer(22-228)metadata(39-53)get_last_install_cluster_events(77-88)get_events_by_host(126-132)get_host_log_file(134-165)assisted_service_mcp/src/utils/log_analyzer/main.py (1)
analyze_cluster(14-71)assisted_service_mcp/src/utils/log_analyzer/signatures/basic_info.py (1)
ComponentsVersionSignature(14-55)assisted_service_mcp/src/utils/log_analyzer/signatures/error_detection.py (8)
analyze(39-62)analyze(72-90)analyze(98-118)analyze(126-150)analyze(158-185)analyze(191-213)analyze(229-260)SNOHostnameHasEtcd(36-62)assisted_service_mcp/src/utils/log_analyzer/signatures/networking.py (4)
analyze(30-71)analyze(78-178)analyze(220-248)SNOMachineCidrSignature(27-71)
assisted_service_mcp/src/tools/network_tools.py (5)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (4)
InventoryClient(27-557)get_infra_env(217-232)update_infra_env(323-344)list_infra_envs(235-253)assisted_service_mcp/src/utils/static_net/template.py (2)
NMStateTemplateParams(102-118)generate_nmstate_from_template(121-124)assisted_service_mcp/src/utils/static_net/config.py (3)
add_or_replace_static_host_config_yaml(41-70)remove_static_host_config_by_index(23-38)validate_and_parse_nmstate(96-108)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
assisted_service_mcp/src/tools/download_tools.py (3)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (3)
list_infra_envs(235-253)get_infra_env_download_url(533-557)get_presigned_for_cluster_credentials(501-530)assisted_service_mcp/utils/helpers.py (1)
format_presigned_url(11-36)
assisted_service_mcp/src/tools/host_tools.py (3)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (2)
InventoryClient(27-557)update_host(470-498)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
tests/test_mcp.py (1)
assisted_service_mcp/src/mcp.py (2)
AssistedServiceMCPServer(27-168)list_tools_sync(156-168)
tests/test_api.py (1)
assisted_service_mcp/src/metrics/metrics.py (1)
metrics(69-71)
assisted_service_mcp/src/tools/operator_tools.py (2)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (3)
InventoryClient(27-557)get_operator_bundles(427-437)add_operator_bundle_to_cluster(440-467)
tests/test_metrics.py (1)
assisted_service_mcp/src/metrics/metrics.py (3)
metrics(69-71)initiate_metrics(44-48)track_tool_usage(51-65)
assisted_service_mcp/src/tools/cluster_tools.py (5)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (8)
InventoryClient(27-557)get_cluster(117-141)list_clusters(144-154)create_cluster(256-295)create_infra_env(298-320)update_cluster(347-385)install_cluster(388-403)update_infra_env(323-344)assisted_service_mcp/src/service_client/helpers.py (1)
Helpers(7-38)assisted_service_mcp/src/utils/log_analyzer/main.py (1)
analyze_cluster(14-71)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
assisted_service_mcp/src/tools/event_tools.py (2)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (1)
get_events(173-214)
assisted_service_mcp/src/mcp.py (5)
assisted_service_mcp/utils/auth.py (2)
get_offline_token(10-45)get_access_token(48-117)assisted_service_mcp/src/settings.py (1)
get_setting(193-202)assisted_service_mcp/src/tools/cluster_tools.py (5)
cluster_info(15-51)list_clusters(55-91)create_cluster(95-218)set_cluster_vips(222-272)install_cluster(316-350)assisted_service_mcp/src/tools/network_tools.py (4)
validate_nmstate_yaml(22-49)generate_nmstate_yaml(53-90)alter_static_network_config_nmstate_for_host(94-164)list_static_network_config(168-204)assisted_service_mcp/src/utils/static_net/template.py (1)
NMStateTemplateParams(102-118)
tests/test_assisted_service_api.py (2)
assisted_service_mcp/src/service_client/assisted_service_api.py (1)
InventoryClient(27-557)assisted_service_mcp/src/service_client/exceptions.py (1)
AssistedServiceAPIError(15-16)
assisted_service_mcp/src/utils/log_analyzer/main.py (1)
assisted_service_mcp/src/service_client/assisted_service_api.py (1)
InventoryClient(27-557)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Red Hat Konflux / assisted-service-mcp-saas-main-on-pull-request
🔇 Additional comments (31)
assisted_service_mcp/src/service_client/helpers.py (1)
19-19: LGTM! Documentation now matches implementation.The docstring update accurately reflects that the
platformparameter acceptsOptional[str]and defaults to baremetal whenNone, which matches both the function signature and the actual implementation.assisted_service_mcp/src/utils/log_analyzer/signatures/error_detection.py (1)
13-16: LGTM! Import path correctly updated for new module structure.The absolute import path reflects the refactored module layout under
assisted_service_mcp.src.utils.log_analyzer. Multiline formatting with a trailing comma follows PEP 8 best practices.assisted_service_mcp/src/utils/log_analyzer/signatures/advanced_analysis.py (1)
11-14: LGTM! Import path correctly updated for new module structure.The absolute import path reflects the refactored module layout under
assisted_service_mcp.src.utils.log_analyzer. The change is consistent with the refactoring across other signature files.assisted_service_mcp/src/utils/log_analyzer/signatures/networking.py (1)
13-20: LGTM! Import paths correctly updated for new module structure.Both import statements reflect the refactored module layout:
- Constants from
assisted_service_mcp.src.utils.log_analyzer.log_analyzer- Helper functions from
assisted_service_mcp.src.utils.log_analyzer.signatures.advanced_analysisThe changes maintain correct intra-package dependencies and follow consistent formatting.
assisted_service_mcp/src/utils/static_net/config.py (2)
34-35: LGTM! IndexError is semantically correct.Changing from
ValueErrortoIndexErrorfor out-of-range index access aligns with Python conventions and makes error handling more precise for callers.
84-84: Good defensive coding: require both fields.Tightening the filter to require both
"mac-address"and"name"prevents incomplete entries inname_and_mac_listand aligns with theMacInterfaceMapTypedDict contract.assisted_service_mcp/src/tools/__init__.py (1)
1-1: LGTM!The module docstring appropriately documents the package purpose for MCP tools.
assisted_service_mcp/src/utils/log_analyzer/main.py (1)
8-8: LGTM!The import path correctly reflects the new package structure.
assisted_service_mcp/src/tools/host_tools.py (1)
12-60: LGTM!The
set_host_rolefunction is well-implemented with:
- Clear type annotations using Annotated and Field
- Comprehensive docstring with Prerequisites, Related tools, and Returns sections
- Proper metrics tracking via the decorator
- Appropriate logging at key points
- Clean implementation flow using the shared helper
pyproject.toml (3)
17-18: LGTM!The dependency updates correctly support the new modular architecture:
pydantic-settingsandpython-dotenvenable the centralized configuration modulefastapiis properly placed in main dependencies for runtime usetypes-requestscorrectly moved to dev dependencies only- All versions align with latest stable releases
Based on learnings
Also applies to: 21-21, 33-34
55-55: LGTM!The testing and coverage configuration is well-structured:
- Appropriately ignores integration tests from linting
- Allows runtime imports where necessary
- Enables coverage tracking with detailed reporting
- Correctly omits entry point modules from coverage
Also applies to: 66-66, 74-74, 81-85
93-93: LGTM!The mypy exclude path correctly reflects the new package structure.
tests/test_log_analyzer.py (7)
6-17: LGTM!The
make_archivehelper provides a clean test fixture with proper handling of the unusedkwargsparameter (linting warning addressed).
20-69: LGTM!The test comprehensively validates LogAnalyzer's metadata and event processing:
- Deleted host separation based on installation start time
- Event partitioning around reset events
- Host-based event grouping
72-89: LGTM!The test validates the host log path resolution with proper fallback from new to old format.
92-112: LGTM!The test validates the main analyze_cluster flow with appropriate mocking and happy-path verification.
115-130: LGTM!The test provides smoke test coverage for the ComponentsVersionSignature without overspecifying expected outputs.
133-148: LGTM!The test ensures the SNOHostnameHasEtcd signature handles minimal data without crashing.
151-165: LGTM!The test ensures the SNOMachineCidrSignature handles minimal data without crashing.
assisted_service_mcp/src/utils/static_net/template.py (3)
14-16: LGTM!The Field validation improvements add important safety constraints:
- Bond ports require minimum 2 interfaces (correct for bonding)
- VLAN ID range 1-4094 matches IEEE 802.1Q specification
- Field formatting improvements enhance readability
Also applies to: 66-68, 85-85
124-124: LGTM!Using
**params.model_dump()is the correct approach for Pydantic models, ensuring proper serialization and explicit field passing.
218-218: LGTM!The template whitespace adjustment aligns with YAML formatting conventions.
assisted_service_mcp/src/service_client/exceptions.py (1)
12-12: LGTM!The import path correctly reflects the centralized logger module introduced in the new package structure.
README.md (2)
32-32: LGTM!The VSCode configuration path correctly references the new module entry point.
46-46: LGTM!The SSE startup command correctly uses the module path for execution (previously addressed review comment resolved).
tests/test_assisted_service_api.py (1)
12-14: LGTM: imports and settings patch paths updated correctly.The refactor to assisted_service_mcp.src.* and patching settings.settings.* aligns with the new centralized config.
Also applies to: 61-71, 116-118, 141-141
tests/test_metrics.py (1)
18-59: LGTM: deterministic metric validations with isolated labels.Precise delta checks and unique tool label avoid REGISTRY cross-test interference.
assisted_service_mcp/src/tools/download_tools.py (1)
94-158: LGTM!This function consistently returns JSON for all paths (errors and success), enabling reliable client-side parsing.
assisted_service_mcp/src/tools/network_tools.py (1)
93-164: LGTM!The function correctly handles add, replace, and delete operations with appropriate validation and error handling.
assisted_service_mcp/src/tools/cluster_tools.py (2)
14-350: LGTM!Functions
cluster_info,list_clusters,create_cluster,set_cluster_vips,set_cluster_platform, andinstall_clusterare well-implemented with proper error handling, logging, and validation.
416-426: LGTM!The
analyze_cluster_logsfunction is straightforward and correctly delegates to the log analyzer utility.
| validate_and_parse_nmstate(nmstate_yaml) | ||
| return "YAML is valid" |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Add exception handling for user-friendly error messages.
validate_and_parse_nmstate raises ValueError for invalid YAML. The exception should be caught and returned as a user-friendly error string instead of propagating.
Apply this diff:
- validate_and_parse_nmstate(nmstate_yaml)
- return "YAML is valid"
+ try:
+ validate_and_parse_nmstate(nmstate_yaml)
+ return "YAML is valid"
+ except ValueError as e:
+ log.error("Invalid NMState YAML: %s", e)
+ return f"ERROR: Invalid NMState YAML: {str(e)}"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| validate_and_parse_nmstate(nmstate_yaml) | |
| return "YAML is valid" | |
| try: | |
| validate_and_parse_nmstate(nmstate_yaml) | |
| return "YAML is valid" | |
| except ValueError as e: | |
| log.error("Invalid NMState YAML: %s", e) | |
| return f"ERROR: Invalid NMState YAML: {str(e)}" |
🤖 Prompt for AI Agents
In assisted_service_mcp/src/tools/network_tools.py around lines 48 to 49, the
call to validate_and_parse_nmstate(nmstate_yaml) can raise ValueError for
invalid YAML; wrap this call in a try/except block that catches ValueError and
returns a user-friendly error string (e.g., "Invalid NMState YAML: <error
message>") instead of letting the exception propagate, while keeping the
successful path returning "YAML is valid".
carbonin
left a comment
There was a problem hiding this comment.
I think we should remove the mentions of the OCM offline token from the tool descriptions. This isn't actually necessary (because we typically will use the request token in the header) but also because the model doesn't ever need to know anything about auth. It's all handled outside the parameters the model actually passes.
I also struggle with the maintainability of the Related tools section.
I have a hard time believing that someone is going to remember to go and add a new tool to every other tool's related section when appropriate. Things have been working well without this. Maybe it's not needed?
assisted_service_mcp/src/api.py
Outdated
| server = AssistedServiceMCPServer() | ||
|
|
||
| # Choose the appropriate transport protocol based on settings | ||
| TRANSPORT_VALUE = getattr(settings, "TRANSPORT", "sse") |
There was a problem hiding this comment.
Why set TRANSPORT_VALUE here rather than using settings.TRANSPORT directly?
| cluster_id: Annotated[ | ||
| str, | ||
| Field( | ||
| description="The unique identifier of the cluster to retrieve information for. This is typically a UUID string." |
| - Valid OCM offline token for authentication | ||
| - Multi-node cluster on baremetal, vsphere, or nutanix platform | ||
| - Two unused IP addresses within the cluster subnet | ||
| - IPs must be reachable from all cluster nodes |
There was a problem hiding this comment.
This is probably something the model won't know. Is it just here to pass that information on to the user before they use the tool?
If it turns out the VIPs are not reachable that will come back as a validation so we'll get that feedback eventually even if it's not here.
tests/test_integration_api.py
Outdated
tests/test_metrics.py
Outdated
| with TestClient(app) as client: | ||
| resp = client.get("/metrics") | ||
| assert resp.status_code == 200 | ||
| assert "# HELP" in resp.text or "HELP" in resp.text |
There was a problem hiding this comment.
Is this duplicating the test test_metrics_endpoint_present_and_exposes_prometheus from test_apy.py?
ab31f1b to
a232afa
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (6)
assisted_service_mcp/src/tools/network_tools.py (3)
75-80: Include exception details in error returns for generation failuresReturn the exception text to aid troubleshooting.
- except TemplateError as e: - log.error("Failed to render nmstate template", exc_info=e) - return "ERROR: Failed to generate nmstate yaml" - except Exception as e: - log.error("Exception generating nmstate yaml", exc_info=e) - return "ERROR: Unknown error" + except TemplateError as e: + log.error("Failed to render nmstate template", exc_info=e) + return f"ERROR: Failed to generate nmstate yaml: {str(e)}" + except Exception as e: + log.error("Exception generating nmstate yaml", exc_info=e) + return f"ERROR: Failed to generate nmstate yaml: {str(e)}"
43-44: Handle invalid YAML with a user-friendly error instead of raisingWrap validate_and_parse_nmstate to catch ValueError and return a clear message; avoid unhandled exceptions from the tool.
- validate_and_parse_nmstate(nmstate_yaml) - return "YAML is valid" + try: + validate_and_parse_nmstate(nmstate_yaml) + return "YAML is valid" + except ValueError as e: + log.error("Invalid NMState YAML: %s", e) + return f"ERROR: Invalid NMState YAML: {str(e)}"
174-180: Bug: double-encoded/incorrect static_network_config response handlingAPI returns static_network_config as a JSON string; json.dumps here produces a JSON string literal, not an array. Handle str/list/None robustly and return a JSON array string.
- return json.dumps(infra_envs[0].get("static_network_config", [])) + value = infra_envs[0].get("static_network_config") + if value is None: + return "[]" + if isinstance(value, str): + # API returns JSON string; return as-is + return value + if isinstance(value, list): + # Already a list; serialize to JSON array + return json.dumps(value) + log.warning( + "Unexpected type for static_network_config: %s", + type(value).__name__, + ) + return "[]"Based on learnings
assisted_service_mcp/src/tools/download_tools.py (1)
69-117: Standardize error/no-results to JSON to match success pathReturn JSON consistently; callers can always json.loads the result.
- except Exception as e: - log.error("Failed to retrieve infrastructure environments: %s", e) - return f"Error retrieving ISO URLs: {str(e)}" + except Exception as e: + log.error("Failed to retrieve infrastructure environments: %s", e) + return json.dumps({"error": f"Error retrieving ISO URLs: {str(e)}"}) @@ - if not infra_envs: + if not infra_envs: log.info("No infrastructure environments found for cluster %s", cluster_id) - return "No ISO download URLs found for this cluster." + return json.dumps({"error": "No ISO download URLs found for this cluster."}) @@ - if not iso_info: + if not iso_info: log.info( "No ISO download URLs found in infrastructure environments for cluster %s", cluster_id, ) - return "No ISO download URLs found for this cluster." + return json.dumps({"error": "No ISO download URLs found for this cluster."})assisted_service_mcp/src/tools/cluster_tools.py (2)
7-12: Move _get_cluster_infra_env_id import to top-level (avoid C0415, clearer deps)No circular dependency detected; import once at module level.
from assisted_service_mcp.src.service_client.assisted_service_api import InventoryClient from assisted_service_mcp.src.service_client.helpers import Helpers from assisted_service_mcp.src.logger import log from assisted_service_mcp.src.utils.log_analyzer.main import analyze_cluster +from assisted_service_mcp.src.tools.shared_helpers import _get_cluster_infra_env_id
340-341: Remove local import now that it’s at module scopeCleaning up the duplicate/local import.
- # Import helper function here to avoid circular imports - from assisted_service_mcp.src.tools.shared_helpers import _get_cluster_infra_env_id + # _get_cluster_infra_env_id imported at module level
🧹 Nitpick comments (11)
tests/test_metrics.py (1)
50-74: Consider strengthening idempotency validation.The test verifies that calling
initiate_metricsmultiple times doesn't error and that labels remain present, but it doesn't validate the precise idempotency guarantees. The comment on line 69 acknowledges that "count may increase by 1," but the test doesn't verify this behavior or check for unwanted side effects.Consider reading metric values before and after the second
initiate_metricscall to verify the exact behavior (e.g., that counters increment by exactly 1 as documented, or remain unchanged if that's the intended behavior).Example strengthening:
# Calling initiate_metrics again should be harmless (idempotent in the sense of no error) - # Note: current implementation also adds an observation (0), so count may increase by 1. + # Note: current implementation increments counter by 1 and adds histogram observation of 0. + before_second_init = _read_metric_value("assisted_service_mcp_tool_request_count_total", tool) initiate_metrics([tool]) + after_second_init = _read_metric_value("assisted_service_mcp_tool_request_count_total", tool) + assert after_second_init == before_second_init + 1.0, "initiate_metrics should increment counter by exactly 1" _ = generate_latest()Where
_read_metric_valueis a helper that extracts a specific metric value for a given tool label..env.template (3)
35-35: Default file logging to false for containersWriting logs to files by default is risky in containers (disk usage, rotated logs). Recommend default LOG_TO_FILE=false and enable only when needed.
37-39: Unify boolean conventionsOther booleans use true/false while ENABLE_TROUBLESHOOTING_TOOLS uses 0/1. Prefer consistent true/false across the template to reduce confusion (pydantic parses both).
12-19: Optional: Satisfy dotenv-linter key orderReorder keys so CLIENT_DEBUG appears before INVENTORY_URL to appease dotenv-linter. No runtime impact.
assisted_service_mcp/src/tools/operator_tools.py (2)
41-46: Avoid hardcoding allowed bundle names in docsEnumerating 'virtualization' and 'openshift-ai' can drift from backend reality. Prefer generic wording and instruct callers to use list_operator_bundles() first, or validate against the live list before update.
64-73: Optional: Validate bundle name before update for better UXPre-fetch bundles and validate bundle_name to return a clear error early instead of a backend error.
Example:
bundles = {b["name"] for b in await client.get_operator_bundles()} if bundle_name not in bundles: return f"ERROR: Unknown bundle '{bundle_name}'. Use list_operator_bundles first."tests/test_tools_module.py (1)
25-36: Trim unnecessary server instantiation and token patches in unit testsMost tests call tool functions directly and don’t rely on the MCP wrapper. You can drop AssistedServiceMCPServer() construction and get_access_token patches in these cases to speed tests and reduce noise (pattern repeats throughout file).
assisted_service_mcp/src/tools/network_tools.py (2)
126-141: Catch IndexError for out-of-range host index and return a clear messageInvalid indexes currently raise and crash the tool. Catch and return a concise error.
- if new_nmstate_yaml is None: - if index is None: - raise ValueError("index cannot be null when removing a host yaml") - if not infra_env.static_network_config: - raise ValueError( - "cannot remove host yaml with empty existing static network config" - ) - static_network_config = remove_static_host_config_by_index( - existing_static_network_config=infra_env.static_network_config, index=index - ) - else: - static_network_config = add_or_replace_static_host_config_yaml( - existing_static_network_config=infra_env.static_network_config, - index=index, - new_nmstate_yaml=new_nmstate_yaml, - ) + try: + if new_nmstate_yaml is None: + if index is None: + raise ValueError("index cannot be null when removing a host yaml") + if not infra_env.static_network_config: + raise ValueError( + "cannot remove host yaml with empty existing static network config" + ) + static_network_config = remove_static_host_config_by_index( + existing_static_network_config=infra_env.static_network_config, index=index + ) + else: + static_network_config = add_or_replace_static_host_config_yaml( + existing_static_network_config=infra_env.static_network_config, + index=index, + new_nmstate_yaml=new_nmstate_yaml, + ) + except IndexError as e: + log.error("Invalid static network config index: %s", e) + return f"ERROR: {str(e)}"
174-179: Prefer using shared helper to select InfraEnv instead of requiring exactly oneCurrent check rejects clusters with multiple InfraEnvs. Consider using _get_cluster_infra_env_id to pick the first valid one and proceed.
- if len(infra_envs) != 1: - log.warning( - "cluster %s has %d infra_envs, expected 1", cluster_id, len(infra_envs) - ) - return "ERROR: this cluster doesn't have exactly 1 infra env, cannot manage static network config" + if not infra_envs: + log.error("No InfraEnvs found for cluster %s", cluster_id) + return "ERROR: No InfraEnvs found for this cluster" + if len(infra_envs) > 1: + log.warning( + "Found %d InfraEnvs for cluster %s; using the first valid one", + len(infra_envs), + cluster_id, + )assisted_service_mcp/src/tools/download_tools.py (2)
91-99: Skip InfraEnv entries without an ID to avoid unnecessary API errorsGuard against missing IDs before calling get_infra_env_download_url.
for infra_env in infra_envs: - infra_env_id = infra_env.get("id", "unknown") - - try: + infra_env_id = infra_env.get("id") + if not infra_env_id: + log.warning("Skipping InfraEnv without id: %s", infra_env) + continue + try: presigned_url = await client.get_infra_env_download_url(infra_env_id)
17-31: Docstring mismatch: returns a dict, not a stringClarify return type description.
- Format a presigned URL object into a readable string. + Format a PresignedUrl into a JSON‑serializable dict. @@ - Returns: - dict: A dict containing URL and optional expiration time. + Returns: + dict: A dict containing URL and optional expiration time.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
.env.template(1 hunks)assisted_service_mcp/src/api.py(1 hunks)assisted_service_mcp/src/mcp.py(1 hunks)assisted_service_mcp/src/tools/cluster_tools.py(1 hunks)assisted_service_mcp/src/tools/download_tools.py(1 hunks)assisted_service_mcp/src/tools/event_tools.py(1 hunks)assisted_service_mcp/src/tools/host_tools.py(1 hunks)assisted_service_mcp/src/tools/network_tools.py(1 hunks)assisted_service_mcp/src/tools/operator_tools.py(1 hunks)assisted_service_mcp/src/tools/version_tools.py(1 hunks)tests/test_metrics.py(1 hunks)tests/test_tools_module.py(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- assisted_service_mcp/src/tools/version_tools.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-09T18:51:46.598Z
Learnt from: keitwb
PR: openshift-assisted/assisted-service-mcp#91
File: service_client/static_net.py:21-36
Timestamp: 2025-09-09T18:51:46.598Z
Learning: In the assisted-service API, the static_network_config field is typed as a list when input to the API but comes back out as a string in responses. Functions processing this field from API responses should handle string inputs only.
Applied to files:
assisted_service_mcp/src/tools/network_tools.py
🧬 Code graph analysis (10)
assisted_service_mcp/src/mcp.py (4)
assisted_service_mcp/utils/auth.py (2)
get_offline_token(10-45)get_access_token(48-117)assisted_service_mcp/src/settings.py (1)
get_setting(193-202)assisted_service_mcp/src/tools/network_tools.py (1)
generate_nmstate_yaml(48-80)assisted_service_mcp/src/utils/static_net/template.py (1)
NMStateTemplateParams(102-118)
assisted_service_mcp/src/tools/operator_tools.py (3)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (3)
InventoryClient(27-557)get_operator_bundles(427-437)add_operator_bundle_to_cluster(440-467)tests/test_tools_module.py (3)
to_str(451-452)to_str(495-496)to_str(756-757)
assisted_service_mcp/src/tools/network_tools.py (5)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (4)
InventoryClient(27-557)get_infra_env(217-232)update_infra_env(323-344)list_infra_envs(235-253)assisted_service_mcp/src/utils/static_net/template.py (2)
NMStateTemplateParams(102-118)generate_nmstate_from_template(121-124)assisted_service_mcp/src/utils/static_net/config.py (3)
add_or_replace_static_host_config_yaml(41-70)remove_static_host_config_by_index(23-38)validate_and_parse_nmstate(96-108)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
assisted_service_mcp/src/api.py (2)
assisted_service_mcp/src/mcp.py (1)
AssistedServiceMCPServer(27-169)assisted_service_mcp/src/logger.py (1)
configure_logging(132-178)
assisted_service_mcp/src/tools/host_tools.py (3)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (2)
InventoryClient(27-557)update_host(470-498)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
tests/test_metrics.py (1)
assisted_service_mcp/src/metrics/metrics.py (3)
metrics(69-71)initiate_metrics(44-48)track_tool_usage(51-65)
assisted_service_mcp/src/tools/download_tools.py (2)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (3)
list_infra_envs(235-253)get_infra_env_download_url(533-557)get_presigned_for_cluster_credentials(501-530)
assisted_service_mcp/src/tools/event_tools.py (2)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (1)
get_events(173-214)
assisted_service_mcp/src/tools/cluster_tools.py (5)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (8)
InventoryClient(27-557)get_cluster(117-141)list_clusters(144-154)create_cluster(256-295)create_infra_env(298-320)update_cluster(347-385)install_cluster(388-403)update_infra_env(323-344)assisted_service_mcp/src/service_client/helpers.py (1)
Helpers(7-38)assisted_service_mcp/src/utils/log_analyzer/main.py (1)
analyze_cluster(14-71)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
tests/test_tools_module.py (5)
assisted_service_mcp/src/tools/version_tools.py (1)
list_versions(12-31)assisted_service_mcp/src/tools/operator_tools.py (2)
list_operator_bundles(13-32)add_operator_bundle_to_cluster(36-81)assisted_service_mcp/src/service_client/assisted_service_api.py (4)
add_operator_bundle_to_cluster(440-467)update_cluster(347-385)get_operator_bundles(427-437)get_openshift_versions(406-424)assisted_service_mcp/src/mcp.py (1)
AssistedServiceMCPServer(27-169)tests/test_utils.py (1)
create_test_cluster(10-31)
🪛 dotenv-linter (3.3.0)
.env.template
[warning] 18-18: [UnorderedKey] The CLIENT_DEBUG key should go before the INVENTORY_URL key
(UnorderedKey)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Red Hat Konflux / assisted-service-mcp-saas-main-on-pull-request
🔇 Additional comments (6)
tests/test_metrics.py (2)
7-48: Well-structured test with effective isolation.The test properly addresses previous concerns about global registry pollution by using a unique tool name and validating exact increments. The pattern of mutating
__name__(line 16) to control the decorator's label is unconventional but necessary given thattrack_tool_usageusesfunc.__name__for labeling. This approach is acceptable in a test context where you need precise control over the label.The defensive handling of potentially-None initial values (lines 44-47) is good practice and ensures the test works correctly even if the metric doesn't exist before the first call.
1-74: Clarification on test duplication concern.There’s no
test_metricsintest_apy.py, so no overlap exists. These are unit tests for the decorator andinitiate_metrics; they complement (not duplicate) any HTTP/metricsendpoint tests.assisted_service_mcp/src/tools/host_tools.py (1)
51-53: Verify update parameter name:rolevshost_roleInventoryClient.update_host builds HostUpdateParams(**update_params). Many Assisted Service SDKs expect
role, nothost_role. If the model doesn’t accepthost_role, this will fail at runtime.If your models use
role, change to:- result = await client.update_host(host_id, infra_env_id, host_role=role) + result = await client.update_host(host_id, infra_env_id, role=role)Please confirm against the generated model.
assisted_service_mcp/src/tools/event_tools.py (2)
11-41: LGTM for event retrieval flowToken fetch, client calls, logging, and error propagation are clean.
44-89: LGTM for host-specific eventsConsistent with cluster_events and InventoryClient API.
assisted_service_mcp/src/mcp.py (1)
121-151: Wrapper and signature shaping look solidOffloading token generation avoids blocking, and signature trimming keeps tool surfaces clean in MCP.
a232afa to
731d4a1
Compare
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (4)
assisted_service_mcp/src/api.py (1)
18-23: Normalize transport and add fallback for consistency with mcp.py.The transport comparison is case-sensitive here but normalized in mcp.py (line 38). This inconsistency could cause mismatched behavior. Normalize the value and default to SSE for unknown values.
Apply this diff:
-# Choose the appropriate transport protocol based on settings -if settings.TRANSPORT == "streamable-http": - app = server.mcp.streamable_http_app() - log.info("Using StreamableHTTP transport (stateless)") -else: - app = server.mcp.sse_app() - log.info("Using SSE transport (stateful)") +transport = (settings.TRANSPORT or "").lower() +if transport == "streamable-http": + app = server.mcp.streamable_http_app() + log.info("Using StreamableHTTP transport (stateless)") +elif transport in ("", "sse"): + app = server.mcp.sse_app() + log.info("Using SSE transport (stateful)") +else: + app = server.mcp.sse_app() + log.warning("Unknown TRANSPORT=%r; defaulting to SSE", settings.TRANSPORT)assisted_service_mcp/src/tools/network_tools.py (1)
170-180: Fix double-encoding bug when static_network_config is already a JSON string.According to learnings, the API returns
static_network_configas a string in responses but accepts it as a list on input. Callingjson.dumpson a string value produces a JSON string literal (e.g.,"\"[...]\""), not an array. Handle the type properly.Based on learnings.
Apply this diff:
- return json.dumps(infra_envs[0].get("static_network_config", [])) + value = infra_envs[0].get("static_network_config") + if value is None: + return "[]" + if isinstance(value, str): + # API returns JSON string; return as-is + return value + # Fallback: serialize list/dict to JSON + return json.dumps(value)assisted_service_mcp/src/tools/download_tools.py (1)
69-117: Inconsistent return format still present.This function returns plain strings for error/no-results cases (lines 76, 80, 114) but JSON for success (line 117), which breaks callers expecting consistent JSON parsing. This issue was flagged in a previous review but remains unresolved.
Apply the previously suggested fix to return JSON in all cases:
try: token = get_access_token_func() client = InventoryClient(token) infra_envs = await client.list_infra_envs(cluster_id) except Exception as e: log.error("Failed to retrieve infrastructure environments: %s", e) - return f"Error retrieving ISO URLs: {str(e)}" + return json.dumps({"error": f"Error retrieving ISO URLs: {str(e)}"}) if not infra_envs: log.info("No infrastructure environments found for cluster %s", cluster_id) - return "No ISO download URLs found for this cluster." + return json.dumps({"error": "No ISO download URLs found for this cluster."}) ... if not iso_info: log.info( "No ISO download URLs found in infrastructure environments for cluster %s", cluster_id, ) - return "No ISO download URLs found for this cluster." + return json.dumps({"error": "No ISO download URLs found for this cluster."}) log.info("Returning %d ISO URLs for cluster %s", len(iso_info), cluster_id) return json.dumps(iso_info)assisted_service_mcp/src/tools/cluster_tools.py (1)
340-341: Local import should be moved to top-level.The import of
_get_cluster_infra_env_idis still inside the function despite a previous review confirming no circular dependency exists (the verification script found zero imports fromcluster_toolsinshared_helpers.py). Moving this to the module-level imports improves consistency and avoids pylint C0415.Move the import to the top of the file:
from assisted_service_mcp.src.metrics import track_tool_usage from assisted_service_mcp.src.service_client.assisted_service_api import InventoryClient from assisted_service_mcp.src.service_client.helpers import Helpers from assisted_service_mcp.src.logger import log from assisted_service_mcp.src.utils.log_analyzer.main import analyze_cluster +from assisted_service_mcp.src.tools.shared_helpers import _get_cluster_infra_env_idThen remove lines 340-341 from the function body.
🧹 Nitpick comments (1)
assisted_service_mcp/src/tools/network_tools.py (1)
43-44: Add exception handling for user-friendly validation errors.
validate_and_parse_nmstateraisesValueErrorfor invalid YAML. Catch this and return a descriptive error string instead of propagating the exception, which provides a better user experience.Apply this diff:
- validate_and_parse_nmstate(nmstate_yaml) - return "YAML is valid" + try: + validate_and_parse_nmstate(nmstate_yaml) + return "YAML is valid" + except ValueError as e: + log.error("Invalid NMState YAML: %s", e) + return f"ERROR: Invalid NMState YAML: {str(e)}"
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
.env.template(1 hunks)assisted_service_mcp/src/api.py(1 hunks)assisted_service_mcp/src/mcp.py(1 hunks)assisted_service_mcp/src/tools/cluster_tools.py(1 hunks)assisted_service_mcp/src/tools/download_tools.py(1 hunks)assisted_service_mcp/src/tools/event_tools.py(1 hunks)assisted_service_mcp/src/tools/host_tools.py(1 hunks)assisted_service_mcp/src/tools/network_tools.py(1 hunks)assisted_service_mcp/src/tools/operator_tools.py(1 hunks)assisted_service_mcp/src/tools/version_tools.py(1 hunks)tests/test_metrics.py(1 hunks)tests/test_tools_module.py(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- tests/test_metrics.py
🚧 Files skipped from review as they are similar to previous changes (1)
- assisted_service_mcp/src/tools/version_tools.py
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: carbonin
PR: openshift-assisted/assisted-service-mcp#111
File: pyproject.toml:9-9
Timestamp: 2025-09-25T19:01:36.933Z
Learning: The `mcp` Python package (mcp>=1.15.0) includes FastMCP functionality and provides the same import path `from mcp.server.fastmcp import FastMCP` for backward compatibility with the standalone `fastmcp` package. This allows drop-in replacement when migrating from `fastmcp>=2.8.0` to `mcp>=1.15.0` without requiring code changes.
📚 Learning: 2025-09-09T18:51:46.598Z
Learnt from: keitwb
PR: openshift-assisted/assisted-service-mcp#91
File: service_client/static_net.py:21-36
Timestamp: 2025-09-09T18:51:46.598Z
Learning: In the assisted-service API, the static_network_config field is typed as a list when input to the API but comes back out as a string in responses. Functions processing this field from API responses should handle string inputs only.
Applied to files:
assisted_service_mcp/src/tools/network_tools.py
🧬 Code graph analysis (9)
assisted_service_mcp/src/tools/host_tools.py (3)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (2)
InventoryClient(27-557)update_host(470-498)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
tests/test_tools_module.py (10)
assisted_service_mcp/src/tools/version_tools.py (1)
list_versions(12-31)assisted_service_mcp/src/tools/operator_tools.py (2)
list_operator_bundles(13-32)add_operator_bundle_to_cluster(36-81)assisted_service_mcp/src/service_client/assisted_service_api.py (16)
add_operator_bundle_to_cluster(440-467)update_cluster(347-385)create_cluster(256-295)install_cluster(388-403)get_events(173-214)list_infra_envs(235-253)get_infra_env_download_url(533-557)get_presigned_for_cluster_credentials(501-530)update_host(470-498)get_operator_bundles(427-437)get_infra_env(217-232)get_openshift_versions(406-424)create_infra_env(298-320)update_infra_env(323-344)get_cluster(117-141)list_clusters(144-154)assisted_service_mcp/src/mcp.py (1)
AssistedServiceMCPServer(27-169)assisted_service_mcp/src/tools/cluster_tools.py (7)
set_cluster_platform(247-277)set_cluster_vips(200-243)create_cluster(80-196)install_cluster(281-307)set_cluster_ssh_key(311-364)cluster_info(15-44)list_clusters(48-76)assisted_service_mcp/src/tools/event_tools.py (2)
cluster_events(12-41)host_events(45-89)assisted_service_mcp/src/tools/download_tools.py (2)
cluster_iso_download_url(46-117)cluster_credentials_download_url(121-178)assisted_service_mcp/src/tools/host_tools.py (1)
set_host_role(13-54)assisted_service_mcp/src/tools/network_tools.py (4)
validate_nmstate_yaml(22-44)alter_static_network_config_nmstate_for_host(84-146)list_static_network_config(150-180)generate_nmstate_yaml(48-80)tests/test_utils.py (1)
create_test_cluster(10-31)
assisted_service_mcp/src/mcp.py (3)
assisted_service_mcp/utils/auth.py (2)
get_offline_token(10-45)get_access_token(48-117)assisted_service_mcp/src/settings.py (1)
get_setting(193-202)assisted_service_mcp/src/tools/cluster_tools.py (3)
cluster_info(15-44)list_clusters(48-76)create_cluster(80-196)
assisted_service_mcp/src/tools/event_tools.py (2)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (1)
get_events(173-214)
assisted_service_mcp/src/tools/network_tools.py (5)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (4)
InventoryClient(27-557)get_infra_env(217-232)update_infra_env(323-344)list_infra_envs(235-253)assisted_service_mcp/src/utils/static_net/template.py (2)
NMStateTemplateParams(102-118)generate_nmstate_from_template(121-124)assisted_service_mcp/src/utils/static_net/config.py (3)
add_or_replace_static_host_config_yaml(41-70)remove_static_host_config_by_index(23-38)validate_and_parse_nmstate(96-108)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
assisted_service_mcp/src/tools/download_tools.py (2)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (3)
list_infra_envs(235-253)get_infra_env_download_url(533-557)get_presigned_for_cluster_credentials(501-530)
assisted_service_mcp/src/tools/cluster_tools.py (5)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (8)
InventoryClient(27-557)get_cluster(117-141)list_clusters(144-154)create_cluster(256-295)create_infra_env(298-320)update_cluster(347-385)install_cluster(388-403)update_infra_env(323-344)assisted_service_mcp/src/service_client/helpers.py (1)
Helpers(7-38)assisted_service_mcp/src/utils/log_analyzer/main.py (1)
analyze_cluster(14-71)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
assisted_service_mcp/src/api.py (2)
assisted_service_mcp/src/mcp.py (1)
AssistedServiceMCPServer(27-169)assisted_service_mcp/src/logger.py (1)
configure_logging(132-178)
assisted_service_mcp/src/tools/operator_tools.py (2)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (3)
InventoryClient(27-557)get_operator_bundles(427-437)add_operator_bundle_to_cluster(440-467)
🪛 dotenv-linter (3.3.0)
.env.template
[warning] 18-18: [UnorderedKey] The CLIENT_DEBUG key should go before the INVENTORY_URL key
(UnorderedKey)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Red Hat Konflux / assisted-service-mcp-saas-main-on-pull-request
🔇 Additional comments (8)
assisted_service_mcp/src/tools/host_tools.py (1)
12-54: LGTM! Well-structured host role management.The function properly uses the shared InfraEnv ID helper, handles the token factory pattern correctly, and provides good logging for operations.
assisted_service_mcp/src/tools/operator_tools.py (1)
12-81: LGTM! Clean operator bundle management implementation.Both functions follow the established pattern with proper error handling, logging, and JSON serialization. The hardcoded bundle names in line 44 provide helpful guidance to users, though they could become outdated if new bundles are added.
tests/test_tools_module.py (1)
1-784: LGTM! Comprehensive test coverage for MCP tools.The test suite provides excellent coverage of tool functionalities including cluster, event, download, host, network, operator, and version tools. Good use of mocking and proper testing of both success and error paths.
assisted_service_mcp/src/tools/event_tools.py (1)
11-89: LGTM! Well-implemented event retrieval tools.Both
cluster_eventsandhost_eventsfollow the established pattern with proper error handling, informative logging, and consistent signatures. The exception propagation ensures upstream callers can handle failures appropriately.assisted_service_mcp/src/mcp.py (1)
27-169: LGTM! Robust MCP server implementation.The server initialization, tool registration, and wrapper pattern are well-implemented. Key strengths:
- Transport configuration is properly normalized (line 38)
- Token generation is correctly offloaded to a thread pool to avoid blocking the event loop (lines 133-137)
- Comprehensive tool registration across all functional domains
- Clean separation between sync and async tool listing methods with proper runtime loop detection
assisted_service_mcp/src/tools/download_tools.py (1)
120-178: Excellent: consistent JSON return format.Unlike
cluster_iso_download_url, this function correctly returns JSON in all cases—errors at lines 164 and 170 usejson.dumps({"error": ...}), and success at line 178 usesjson.dumps(format_presigned_url(result)). This consistency makes the function reliable for callers.assisted_service_mcp/src/tools/cluster_tools.py (2)
153-160: Well-designed platform validation.The logic correctly enforces that single-node clusters must use platform
'none'and provides a sensible default ('baremetal'for multi-node,'none'for single-node) when platform is unspecified. The early return with a clear error message prevents invalid configurations.
347-359: Good partial-success error handling.Returning detailed messages when the cluster SSH key update succeeds but the InfraEnv update fails (lines 352, 359) provides valuable feedback to users. This approach acknowledges partial success and includes the updated cluster state, helping users understand what was accomplished and what needs follow-up.
| MCP server for interacting with the OpenShift assisted installer API. | ||
| """ | ||
|
|
||
| __version__ = "0.1.0" |
There was a problem hiding this comment.
What are we using this for? Are we going to ever change it?
There was a problem hiding this comment.
it is coming from the template but I don't really have plans for this
we'd better remove then?
There was a problem hiding this comment.
Yeah if we don't have a use for it then let's just remove it.
assisted_service_mcp/src/mcp.py
Outdated
| """Initialize the MCP server with assisted service tools.""" | ||
| try: | ||
| # Get transport configuration from settings | ||
| use_stateless_http = (settings.TRANSPORT or "").lower() == "streamable-http" |
There was a problem hiding this comment.
The bot comment made me think that maybe we don't need the case translation here since the only values that would validate are the lower case ones, right?
|
|
||
| @pytest.mark.asyncio | ||
| async def test_tool_set_host_role_module() -> None: | ||
| from assisted_service_mcp.src.tools import host_tools |
There was a problem hiding this comment.
Not a big deal but it's strange that these are imported with a different name in each test. I feel like that makes things more confusing, but 🤷
| clusters = await client.list_clusters() | ||
| resp = [ | ||
| { | ||
| "name": cluster.name, |
There was a problem hiding this comment.
This seems to be failing for me when I run this branch locally. The previous implementation used cluster as a dict. Why change this to object notation?
@zszabo-rh can you ensure you run this through some manual tests? I'm worried unit tests will just mock this so that it works the way the function expects.
There was a problem hiding this comment.
The entire previous implementation was:
log.info("Retrieving list of all clusters")
client = InventoryClient(get_access_token())
clusters = await client.list_clusters()
resp = [
{
"name": cluster["name"],
"id": cluster["id"],
"openshift_version": cluster.get("openshift_version", "Unknown"),
"status": cluster["status"],
}
for cluster in clusters
]
log.info("Successfully retrieved %s clusters", len(resp))
return json.dumps(resp)It's rough that stuff like this was changed in this PR because since this was altered and the file was moved I can't actually see the diff, and there's too much here to go and evaluate all of these individually.
We should probably have some kind of integration test that sits between the full eval-test and unit tests that actually runs these to ensure they work in a live environment (kind of like the MCP inspector does)
There was a problem hiding this comment.
ah yes, this one... XD
I think it was changed while I was working around some odd unit test failure earlier. But I’m pretty sure I already fixed this exact issue at some point, wonder when did it sneak back? The server was definitely working perfectly before.
To be fair, I’ve only been running the unit tests lately, so that might explain why I didn’t catch it.
I can try adding some integration-style tests, but do you think that would really make sense? The eval tests already validate actual tool calls, so if something were truly off there, we’d probably see it in the results.
There was a problem hiding this comment.
I don't think it's something we need to do for this PR I was just thinking for the future.
731d4a1 to
9485aaa
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (3)
assisted_service_mcp/src/tools/network_tools.py (2)
43-44: Add exception handling for user-friendly error messages.
validate_and_parse_nmstatecan raiseValueErrorfor invalid YAML. The exception should be caught and returned as a user-friendly error string instead of propagating to the MCP client.Apply this diff:
- validate_and_parse_nmstate(nmstate_yaml) - return "YAML is valid" + try: + validate_and_parse_nmstate(nmstate_yaml) + return "YAML is valid" + except ValueError as e: + log.error("Invalid NMState YAML: %s", e) + return f"ERROR: Invalid NMState YAML: {str(e)}"
180-180: Bug: double-encoded JSON returned for static_network_config.When the API returns a JSON string,
json.dumpswraps it again, producing a JSON string literal rather than a JSON array. This will break client-side parsing. According to the retrieved learning, "the static_network_config field is typed as a list when input to the API but comes back out as a string in responses."Based on learnings.
Apply this diff:
- return json.dumps(infra_envs[0].get("static_network_config", [])) + value = infra_envs[0].get("static_network_config") + if value is None: + return "[]" + if isinstance(value, str): + # API returns JSON string; return as-is + return value + if isinstance(value, list): + # Already a list; serialize to JSON array + return json.dumps(value) + log.warning( + "Unexpected type for static_network_config: %s", + type(value).__name__, + ) + return "[]"assisted_service_mcp/src/tools/download_tools.py (1)
76-76: Inconsistent return format breaks client-side JSON parsing.
cluster_iso_download_urlreturns plain error strings (lines 76, 80, 114) but JSON array on success (line 117). Callers cannot reliably parse the response. In contrast,cluster_credentials_download_urlconsistently returns JSON for all paths (lines 164, 170, 178).Standardize all return values to JSON format:
try: token = get_access_token_func() client = InventoryClient(token) infra_envs = await client.list_infra_envs(cluster_id) except Exception as e: log.error("Failed to retrieve infrastructure environments: %s", e) - return f"Error retrieving ISO URLs: {str(e)}" + return json.dumps({"error": f"Error retrieving ISO URLs: {str(e)}"}) if not infra_envs: log.info("No infrastructure environments found for cluster %s", cluster_id) - return "No ISO download URLs found for this cluster." + return json.dumps({"error": "No ISO download URLs found for this cluster."}) ... if not iso_info: log.info( "No ISO download URLs found in infrastructure environments for cluster %s", cluster_id, ) - return "No ISO download URLs found for this cluster." + return json.dumps({"error": "No ISO download URLs found for this cluster."}) log.info("Returning %d ISO URLs for cluster %s", len(iso_info), cluster_id) return json.dumps(iso_info)Also applies to: 80-80, 114-114
🧹 Nitpick comments (5)
assisted_service_mcp/src/tools/host_tools.py (1)
45-54: Add try/except and use log.exception for parity and better diagnosticsOther tools wrap client calls and re‑raise with traceback. Mirror that here for consistent error telemetry.
@@ - log.info("Setting role '%s' for host %s in cluster %s", role, host_id, cluster_id) - client = InventoryClient(get_access_token_func()) - - # Get the InfraEnv ID for the cluster - infra_env_id = await _get_cluster_infra_env_id(client, cluster_id) - - # Update the host with the specified role - result = await client.update_host(host_id, infra_env_id, host_role=role) - log.info("Successfully set role for host %s in cluster %s", host_id, cluster_id) - return result.to_str() + log.info("Setting role '%s' for host %s in cluster %s", role, host_id, cluster_id) + client = InventoryClient(get_access_token_func()) + try: + # Resolve InfraEnv then update host + infra_env_id = await _get_cluster_infra_env_id(client, cluster_id) + result = await client.update_host(host_id, infra_env_id, host_role=role) + log.info("Successfully set role for host %s in cluster %s", host_id, cluster_id) + return result.to_str() + except Exception: + log.exception( + "Failed to set role '%s' for host %s in cluster %s", role, host_id, cluster_id + ) + raiseassisted_service_mcp/src/tools/operator_tools.py (2)
3-6: Harden JSON serialization and capture tracebacksEnsure results are JSON‑serializable and log full tracebacks on failure.
@@ -import json -from typing import Annotated, Callable +import json +from typing import Annotated, Callable, Any from pydantic import Field @@ +def _to_plain(obj: Any) -> Any: + if hasattr(obj, "to_dict"): + return obj.to_dict() + if isinstance(obj, dict): + return {k: _to_plain(v) for k, v in obj.items()} + if isinstance(obj, (list, tuple)): + return [_to_plain(x) for x in obj] + return obj @@ - result = await client.get_operator_bundles() - log.info("Successfully retrieved %s operator bundles", len(result)) - return json.dumps(result) - except Exception as e: - log.error("Failed to retrieve operator bundles: %s", str(e)) - raise + result = await client.get_operator_bundles() + log.info("Successfully retrieved %s operator bundles", len(result)) + return json.dumps(_to_plain(result)) + except Exception: + log.exception("Failed to retrieve operator bundles") + raise @@ - except Exception as e: - log.error( - "Failed to add operator bundle '%s' to cluster %s: %s", - bundle_name, - cluster_id, - str(e), - ) - raise + except Exception: + log.exception( + "Failed to add operator bundle '%s' to cluster %s", bundle_name, cluster_id + ) + raiseAlso applies to: 24-31, 74-81
41-46: Optional: restrict bundle names via Literal for better UX/validationEnforce the allowed values at the type level so UIs get enum metadata and invalid names are rejected early.
-from typing import Annotated, Callable +from typing import Annotated, Callable, Literal @@ - bundle_name: Annotated[ - str, - Field( - description="The name of the operator bundle to add. The available operator bundle names are 'virtualization' and 'openshift-ai'" - ), - ], + bundle_name: Annotated[ + Literal["virtualization", "openshift-ai"], + Field(description="Choose one of: 'virtualization', 'openshift-ai'"), + ],assisted_service_mcp/src/tools/version_tools.py (1)
13-19: Doc fix: clarify “latest” vs “all”The first line says “List all…”, but the function fetches only latest (True). Update the opening sentence to avoid confusion.
- """List all available OpenShift versions for installation. + """List the latest available OpenShift versions for installation.assisted_service_mcp/src/tools/event_tools.py (1)
1-9: Return a JSON string consistently and log full tracebacksEnsure the functions always return a JSON string (as documented) and capture tracebacks on failure.
@@ -"""Event management tools for Assisted Service MCP Server.""" +"""Event management tools for Assisted Service MCP Server.""" -from typing import Annotated, Callable +import json +from typing import Annotated, Callable, Any from pydantic import Field @@ +def _to_plain(obj: Any) -> Any: + if hasattr(obj, "to_dict"): + return obj.to_dict() + if isinstance(obj, dict): + return {k: _to_plain(v) for k, v in obj.items()} + if isinstance(obj, (list, tuple)): + return [_to_plain(x) for x in obj] + return obj @@ - result = await client.get_events(cluster_id=cluster_id) + result = await client.get_events(cluster_id=cluster_id) log.info("Successfully retrieved events for cluster %s", cluster_id) - return result - except Exception as e: - log.error("Failed to retrieve events for cluster %s: %s", cluster_id, str(e)) - raise + return result if isinstance(result, str) else json.dumps(_to_plain(result)) + except Exception: + log.exception("Failed to retrieve events for cluster %s", cluster_id) + raise @@ - result = await client.get_events(cluster_id=cluster_id, host_id=host_id) + result = await client.get_events(cluster_id=cluster_id, host_id=host_id) log.info( "Successfully retrieved events for host %s in cluster %s", host_id, cluster_id, ) - return result - except Exception as e: - log.error( - "Failed to retrieve events for host %s in cluster %s: %s", - host_id, - cluster_id, - str(e), - ) - raise + return result if isinstance(result, str) else json.dumps(_to_plain(result)) + except Exception: + log.exception( + "Failed to retrieve events for host %s in cluster %s", host_id, cluster_id + ) + raiseAlso applies to: 32-41, 72-89
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
.env.template(1 hunks)assisted_service_mcp/src/api.py(1 hunks)assisted_service_mcp/src/mcp.py(1 hunks)assisted_service_mcp/src/tools/cluster_tools.py(1 hunks)assisted_service_mcp/src/tools/download_tools.py(1 hunks)assisted_service_mcp/src/tools/event_tools.py(1 hunks)assisted_service_mcp/src/tools/host_tools.py(1 hunks)assisted_service_mcp/src/tools/network_tools.py(1 hunks)assisted_service_mcp/src/tools/operator_tools.py(1 hunks)assisted_service_mcp/src/tools/version_tools.py(1 hunks)tests/test_metrics.py(1 hunks)tests/test_tools_module.py(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- tests/test_metrics.py
- assisted_service_mcp/src/api.py
- .env.template
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-09T18:51:46.598Z
Learnt from: keitwb
PR: openshift-assisted/assisted-service-mcp#91
File: service_client/static_net.py:21-36
Timestamp: 2025-09-09T18:51:46.598Z
Learning: In the assisted-service API, the static_network_config field is typed as a list when input to the API but comes back out as a string in responses. Functions processing this field from API responses should handle string inputs only.
Applied to files:
assisted_service_mcp/src/tools/network_tools.py
🔇 Additional comments (4)
assisted_service_mcp/src/mcp.py (2)
133-137: Good: token generation off the event loopOffloading get_access_token with asyncio.to_thread prevents blocking. Wrapper passes a cheap closure—solid approach.
77-79: Verify ENABLE_TROUBLESHOOTING_TOOLS is typed as boolIf get_setting returns strings, any non‑empty value (e.g., "false") will evaluate truthy and unintentionally enable tools. Please verify.
tests/test_tools_module.py (1)
1-784: Comprehensive test coverage for MCP tooling.The test suite provides excellent coverage across cluster, event, download, host, network, version, and operator tools. Tests appropriately use mocks, cover both success and error paths, and validate JSON serialization, error handling, and edge cases.
Regarding the past review comment about import naming inconsistencies: the current implementation uses local imports within each test function (e.g.,
from assisted_service_mcp.src.tools import cluster_tools), which is a reasonable pattern for test isolation and clarity. The naming is now consistent across tests.assisted_service_mcp/src/tools/cluster_tools.py (1)
340-341: Move import to top-level; circular dependency claim unverified.The local import with comment "to avoid circular imports" contradicts the past review verification that found no circular dependency (shared_helpers does not import from cluster_tools). Past review comment at lines 340-341 suggested moving this import to top-level and was marked as addressed in commit 227b497, yet the import remains local.
Move the import to the top of the file:
from assisted_service_mcp.src.service_client.assisted_service_api import InventoryClient from assisted_service_mcp.src.service_client.helpers import Helpers from assisted_service_mcp.src.logger import log from assisted_service_mcp.src.utils.log_analyzer.main import analyze_cluster +from assisted_service_mcp.src.tools.shared_helpers import _get_cluster_infra_env_idAnd remove the local import:
log.info("Setting SSH public key for cluster %s", cluster_id) client = InventoryClient(get_access_token_func()) - # Import helper function here to avoid circular imports - from assisted_service_mcp.src.tools.shared_helpers import _get_cluster_infra_env_id - # Update the cluster with the new SSH public keyVerify no circular dependency exists:
| import json | ||
| from typing import Callable | ||
|
|
There was a problem hiding this comment.
Make return JSON‑safe and log tracebacks
client.get_openshift_versions(True) may return model objects; json.dumps(result) can raise TypeError. Convert to plain types first and use log.exception for full context.
@@
-import json
-from typing import Callable
+import json
+from typing import Callable, Any
@@
+def _to_plain(obj: Any) -> Any:
+ if hasattr(obj, "to_dict"):
+ return obj.to_dict()
+ if isinstance(obj, dict):
+ return {k: _to_plain(v) for k, v in obj.items()}
+ if isinstance(obj, (list, tuple)):
+ return [_to_plain(x) for x in obj]
+ return obj
@@
- result = await client.get_openshift_versions(True)
- log.info("Successfully retrieved OpenShift versions")
- return json.dumps(result)
- except Exception as e:
- log.error("Failed to retrieve OpenShift versions: %s", str(e))
- raise
+ result = await client.get_openshift_versions(True)
+ log.info("Successfully retrieved OpenShift versions")
+ return json.dumps(_to_plain(result))
+ except Exception:
+ log.exception("Failed to retrieve OpenShift versions")
+ raiseAlso applies to: 25-31
🤖 Prompt for AI Agents
In assisted_service_mcp/src/tools/version_tools.py around lines 3-5 (and
similarly for lines 25-31), the code calls json.dumps(result) where result may
contain non-JSON-serializable model objects and currently logs errors without
full tracebacks; wrap the client.get_openshift_versions(True) call and the
json.dumps operation in a try/except, convert the returned models to plain
JSON-safe types before dumping (e.g., call a .to_dict()/as_dict()/serialize
method on each model or build a plain dict/list of primitives), then call
json.dumps on that sanitized data, and on exception use log.exception(...) (not
log.error) to record the full traceback and context.
9485aaa to
0969967
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (4)
assisted_service_mcp/src/tools/network_tools.py (2)
31-44: Catch validation errors and return a clear messagevalidate_and_parse_nmstate raises ValueError on invalid YAML. Mirror generate_nmstate_yaml’s pattern: catch and return a user-facing error string.
- validate_and_parse_nmstate(nmstate_yaml) - return "YAML is valid" + try: + validate_and_parse_nmstate(nmstate_yaml) + return "YAML is valid" + except ValueError as e: + log.error("Invalid NMState YAML: %s", e) + return f"ERROR: Invalid NMState YAML: {str(e)}"
170-180: Bug: double-encoded JSON for static_network_config; handle str/list/None robustlyThe API returns static_network_config as a JSON string in responses. json.dumps here will wrap it again, yielding a JSON string literal rather than an array. Handle types explicitly.
Apply:
- return json.dumps(infra_envs[0].get("static_network_config", [])) + value = infra_envs[0].get("static_network_config") + if value is None: + return "[]" + if isinstance(value, str): + # API returns JSON string; return as-is + return value + if isinstance(value, list): + # Already a list; serialize to JSON array + return json.dumps(value) + log.warning( + "Unexpected type for static_network_config: %s", + type(value).__name__, + ) + return "[]"Based on learnings.
assisted_service_mcp/src/tools/version_tools.py (1)
13-31: Fix JSON serialization (use model.to_str) and align docstring/loggingjson.dumps(result) will fail if result is a model. Return the model’s string representation or convert to dict. Also make the docstring consistent with only_latest=True and log full traceback on error.
-async def list_versions(get_access_token_func: Callable[[], str]) -> str: - """List all available OpenShift versions for installation. +async def list_versions(get_access_token_func: Callable[[], str]) -> str: + """List the latest available OpenShift versions for installation. @@ client = InventoryClient(get_access_token_func()) try: result = await client.get_openshift_versions(True) log.info("Successfully retrieved OpenShift versions") - return json.dumps(result) - except Exception as e: - log.error("Failed to retrieve OpenShift versions: %s", str(e)) + if hasattr(result, "to_str"): + return result.to_str() + if hasattr(result, "to_dict"): + return json.dumps(result.to_dict()) + return json.dumps(result) + except Exception: + log.exception("Failed to retrieve OpenShift versions") raiseassisted_service_mcp/src/tools/download_tools.py (1)
69-117: Inconsistent return format breaks JSON parsing expectations.
cluster_iso_download_urlreturns plain strings for errors and no-results cases:
- Line 76:
f"Error retrieving ISO URLs: {str(e)}"- Lines 80, 114:
"No ISO download URLs found for this cluster."- Line 117:
json.dumps(iso_info)(success)Meanwhile,
cluster_credentials_download_url(lines 164, 170) returns JSON for all cases. This inconsistency was flagged in a previous review and breaks clients expecting uniform JSON responses.Apply this diff to standardize all returns to JSON:
except Exception as e: log.error("Failed to retrieve infrastructure environments: %s", e) - return f"Error retrieving ISO URLs: {str(e)}" + return json.dumps({"error": f"Error retrieving ISO URLs: {str(e)}"}) if not infra_envs: log.info("No infrastructure environments found for cluster %s", cluster_id) - return "No ISO download URLs found for this cluster." + return json.dumps({"error": "No ISO download URLs found for this cluster."}) ... if not iso_info: log.info( "No ISO download URLs found in infrastructure environments for cluster %s", cluster_id, ) - return "No ISO download URLs found for this cluster." + return json.dumps({"error": "No ISO download URLs found for this cluster."})
🧹 Nitpick comments (6)
assisted_service_mcp/src/tools/host_tools.py (1)
45-54: Good flow; consider consistent, user-friendly error returnsThe logic (token injection → infra-env lookup → update_host → to_str) looks solid. To align with network tools’ UX (which return "ERROR: ..." strings on known failures), consider catching ValueError/Request errors here and returning a friendly error string instead of propagating, keeping logs at error level. This keeps tool behavior consistent across the surface.
assisted_service_mcp/src/tools/network_tools.py (1)
122-146: Optional: wrap update flow with targeted error handlingIndex/validation errors (e.g., IndexError, ValueError) currently bubble up. Consider catching and returning "ERROR: ..." strings for predictable tool responses, similar to generate_nmstate_yaml. Keep logs with exc_info for diagnosis.
assisted_service_mcp/src/mcp.py (1)
37-39: Optional: drop redundant .lower() on validated TRANSPORTsettings.TRANSPORT is a Literal validated to lowercase; the extra .lower() and or "" aren’t needed. Purely cosmetic.
tests/test_tools_module.py (3)
1-12: Consider consistent import organization across tests.The module imports
version_toolsandoperator_toolsat the top level (lines 7-11), while other test functions import their respective tool modules locally (e.g.,cluster_toolsat line 16,event_toolsat line 114). This inconsistency was noted in a previous review comment.For better readability and maintainability, consider either importing all tool modules at the top or importing all within test functions consistently.
24-24: Consider using a pytest fixture for AssistedServiceMCPServer instantiation.
AssistedServiceMCPServer()is instantiated in nearly every test function (lines 24, 51, 73, 97, 120, 143, etc.) for its side effects. This creates repetitive setup overhead.Consider extracting this into a pytest fixture to improve test performance and reduce duplication:
@pytest.fixture def mcp_server(): return AssistedServiceMCPServer()Then use
def test_tool_set_cluster_platform_module(mcp_server) -> None:in each test.
523-548: Error branch tests are valid but could be improved.Lines 523-534 and 537-548 test error propagation correctly, but note:
Function signatures use
# type: ignore[no-untyped-def]to suppress missing type hints. Consider adding explicit return type annotations:def test_list_versions_error_branch() -> None: async def run() -> None:The
asyncio.run(run())pattern is fine for synchronous test functions wrapping async logic, but many other tests use@pytest.mark.asynciodirectly. Consider using that pattern consistently for uniformity.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
.env.template(1 hunks)assisted_service_mcp/src/api.py(1 hunks)assisted_service_mcp/src/mcp.py(1 hunks)assisted_service_mcp/src/tools/cluster_tools.py(1 hunks)assisted_service_mcp/src/tools/download_tools.py(1 hunks)assisted_service_mcp/src/tools/event_tools.py(1 hunks)assisted_service_mcp/src/tools/host_tools.py(1 hunks)assisted_service_mcp/src/tools/network_tools.py(1 hunks)assisted_service_mcp/src/tools/operator_tools.py(1 hunks)assisted_service_mcp/src/tools/version_tools.py(1 hunks)tests/test_metrics.py(1 hunks)tests/test_tools_module.py(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- assisted_service_mcp/src/tools/event_tools.py
- assisted_service_mcp/src/tools/operator_tools.py
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: carbonin
PR: openshift-assisted/assisted-service-mcp#111
File: pyproject.toml:9-9
Timestamp: 2025-09-25T19:01:36.933Z
Learning: The `mcp` Python package (mcp>=1.15.0) includes FastMCP functionality and provides the same import path `from mcp.server.fastmcp import FastMCP` for backward compatibility with the standalone `fastmcp` package. This allows drop-in replacement when migrating from `fastmcp>=2.8.0` to `mcp>=1.15.0` without requiring code changes.
📚 Learning: 2025-09-09T18:51:46.598Z
Learnt from: keitwb
PR: openshift-assisted/assisted-service-mcp#91
File: service_client/static_net.py:21-36
Timestamp: 2025-09-09T18:51:46.598Z
Learning: In the assisted-service API, the static_network_config field is typed as a list when input to the API but comes back out as a string in responses. Functions processing this field from API responses should handle string inputs only.
Applied to files:
assisted_service_mcp/src/tools/network_tools.py
🧬 Code graph analysis (9)
tests/test_tools_module.py (11)
assisted_service_mcp/src/tools/version_tools.py (1)
list_versions(12-31)assisted_service_mcp/src/tools/operator_tools.py (2)
list_operator_bundles(13-32)add_operator_bundle_to_cluster(36-81)assisted_service_mcp/src/service_client/assisted_service_api.py (16)
add_operator_bundle_to_cluster(440-467)update_cluster(347-385)create_cluster(256-295)install_cluster(388-403)get_events(173-214)list_infra_envs(235-253)get_infra_env_download_url(533-557)get_presigned_for_cluster_credentials(501-530)update_host(470-498)get_operator_bundles(427-437)get_infra_env(217-232)get_openshift_versions(406-424)create_infra_env(298-320)update_infra_env(323-344)get_cluster(117-141)list_clusters(144-154)assisted_service_mcp/src/mcp.py (1)
AssistedServiceMCPServer(27-169)assisted_service_mcp/src/tools/cluster_tools.py (7)
set_cluster_platform(247-277)set_cluster_vips(200-243)create_cluster(80-196)install_cluster(281-307)set_cluster_ssh_key(311-364)cluster_info(15-44)list_clusters(48-76)assisted_service_mcp/src/tools/event_tools.py (2)
cluster_events(12-41)host_events(45-89)assisted_service_mcp/src/tools/download_tools.py (2)
cluster_iso_download_url(46-117)cluster_credentials_download_url(121-178)assisted_service_mcp/src/tools/host_tools.py (1)
set_host_role(13-54)assisted_service_mcp/src/tools/network_tools.py (4)
validate_nmstate_yaml(22-44)alter_static_network_config_nmstate_for_host(84-146)list_static_network_config(150-180)generate_nmstate_yaml(48-80)assisted_service_mcp/src/utils/static_net/template.py (2)
NMStateTemplateParams(102-118)EthernetInterfaceParams(37-44)tests/test_utils.py (1)
create_test_cluster(10-31)
assisted_service_mcp/src/mcp.py (5)
assisted_service_mcp/utils/auth.py (2)
get_offline_token(10-45)get_access_token(48-117)assisted_service_mcp/src/settings.py (1)
get_setting(193-202)assisted_service_mcp/src/tools/cluster_tools.py (1)
cluster_info(15-44)assisted_service_mcp/src/tools/network_tools.py (1)
generate_nmstate_yaml(48-80)assisted_service_mcp/src/utils/static_net/template.py (1)
NMStateTemplateParams(102-118)
assisted_service_mcp/src/api.py (2)
assisted_service_mcp/src/mcp.py (1)
AssistedServiceMCPServer(27-169)assisted_service_mcp/src/logger.py (1)
configure_logging(132-178)
assisted_service_mcp/src/tools/download_tools.py (2)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (4)
InventoryClient(27-557)list_infra_envs(235-253)get_infra_env_download_url(533-557)get_presigned_for_cluster_credentials(501-530)
assisted_service_mcp/src/tools/network_tools.py (5)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (4)
InventoryClient(27-557)get_infra_env(217-232)update_infra_env(323-344)list_infra_envs(235-253)assisted_service_mcp/src/utils/static_net/template.py (2)
NMStateTemplateParams(102-118)generate_nmstate_from_template(121-124)assisted_service_mcp/src/utils/static_net/config.py (3)
add_or_replace_static_host_config_yaml(41-70)remove_static_host_config_by_index(23-38)validate_and_parse_nmstate(96-108)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
assisted_service_mcp/src/tools/host_tools.py (3)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (2)
InventoryClient(27-557)update_host(470-498)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
tests/test_metrics.py (1)
assisted_service_mcp/src/metrics/metrics.py (3)
metrics(69-71)initiate_metrics(44-48)track_tool_usage(51-65)
assisted_service_mcp/src/tools/version_tools.py (2)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (2)
InventoryClient(27-557)get_openshift_versions(406-424)
assisted_service_mcp/src/tools/cluster_tools.py (5)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (8)
InventoryClient(27-557)get_cluster(117-141)list_clusters(144-154)create_cluster(256-295)create_infra_env(298-320)update_cluster(347-385)install_cluster(388-403)update_infra_env(323-344)assisted_service_mcp/src/service_client/helpers.py (1)
Helpers(7-38)assisted_service_mcp/src/utils/log_analyzer/main.py (1)
analyze_cluster(14-71)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
🪛 dotenv-linter (3.3.0)
.env.template
[warning] 18-18: [UnorderedKey] The CLIENT_DEBUG key should go before the INVENTORY_URL key
(UnorderedKey)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Red Hat Konflux / assisted-service-mcp-saas-main-on-pull-request
🔇 Additional comments (15)
.env.template (1)
1-39: Template is properly aligned with settings.py—no changes needed.Verification confirms all
.env.templatekeys, types, defaults, and optionality correctly match theSettingsmodel inassisted_service_mcp/src/settings.py. Optional fields (OFFLINE_TOKEN, LOGGER_NAME) are appropriately commented out, required fields are present with matching defaults, and type enforcement (int ranges, Literal enums, bool conversion) is properly configured.assisted_service_mcp/src/api.py (1)
11-23: Bootstrap looks solidLogging configured early, server initialized once, and transport selection matches settings. LGTM.
tests/test_metrics.py (2)
7-47: Deterministic metrics assertions with unique labelsGood isolation from global REGISTRY and exact delta assertions. This prevents cross-test interference.
50-74: Idempotence coverage is adequateValidates label presence across re-inits without relying on accumulated counts. LGTM.
assisted_service_mcp/src/mcp.py (1)
133-149: Wrapping pattern avoids event-loop blocking and preserves signaturesOffloading token generation and adjusting signatures is correct and keeps tools clean. LGTM.
tests/test_tools_module.py (1)
681-700: Test validates JSON error format for credentials download.This test correctly verifies that
cluster_credentials_download_urlreturns JSON-formatted errors (lines 698-700). However, note thatcluster_iso_download_urlstill returns plain strings for errors (tested elsewhere), creating an inconsistency that was flagged in past reviews ofdownload_tools.py.assisted_service_mcp/src/tools/download_tools.py (2)
17-42: Clean presigned URL formatting with appropriate zero-datetime filtering.The
format_presigned_urlhelper correctly:
- Filters out meaningless zero/default datetime values (line 37)
- Formats timestamps as ISO 8601 with Z suffix (lines 38-40)
- Returns a consistent dict structure
120-178: Credentials download function follows best practices.This function correctly:
- Returns JSON for all code paths (success, error, no-result)
- Provides comprehensive documentation with Prerequisites and Returns
- Uses appropriate error logging
- Follows the authentication injection pattern via
get_access_token_funcassisted_service_mcp/src/tools/cluster_tools.py (7)
1-12: Imports are well-organized and follow project conventions.All imports are at module level, dependencies are clear, and the structure is clean. The previous issues with local imports have been addressed per past review comments.
14-44: Simple and correct cluster info retrieval.Clean implementation with appropriate logging and documentation. The function correctly delegates to
InventoryClient.get_cluster()and returns the formatted result.
47-76: List clusters implementation is correct.The function correctly accesses clusters as dictionaries (lines 68-71), using
.get()for optional fields likeopenshift_versionwith a sensible default of "Unknown". This matches the API contract whereclient.list_clusters()returns a list of cluster dicts. Test coverage at lines 590-620 in test_tools_module.py validates this behavior.
79-196: Create cluster function handles validation and setup correctly.The function appropriately:
- Validates single-node/platform combinations (lines 153-156), preventing invalid configurations
- Defaults platform based on
single_nodeflag (lines 158-160)- Checks for
cluster.idpresence before proceeding (lines 175-177)- Creates both cluster and infrastructure environment
- Provides comprehensive documentation with examples
199-243: VIP configuration function is well-documented and correct.The function correctly updates cluster VIPs via
update_cluster(). The documentation clearly explains when VIPs are required (baremetal, vsphere, nutanix) and when they're not (SNO, none/oci platforms), which helps prevent user errors.
310-364: SSH key update handles partial failures appropriately.The function correctly:
- Updates cluster SSH key first (line 344)
- Attempts to update InfraEnv with graceful degradation (lines 348-359)
- Returns informative partial-success messages if InfraEnv update fails
- Documents that only new ISO downloads will include the updated key
The local import at lines 340-341 is documented as necessary to avoid circular dependencies and is acceptable. Test coverage validates both success and partial-failure paths (lines 486-520, 704-728 in test_tools_module.py).
367-388: Log analysis function correctly delegates to analyzer utility.Simple and correct implementation that wraps the
analyze_clusterutility and formats results as a human-readable report. The function appropriately returns an empty string when no issues are found.
| INVENTORY_URL=https://api.openshift.com/api/assisted-install/v2 | ||
| # URL endpoint used to fetch the pull secret | ||
| PULL_SECRET_URL=https://api.openshift.com/api/accounts_mgmt/v1/access_token | ||
| # Enable verbose client logging (true/false) | ||
| CLIENT_DEBUG=false |
There was a problem hiding this comment.
Reorder keys within the "Assisted Service API" section.
The static analysis tool flagged that CLIENT_DEBUG (line 18) should appear before INVENTORY_URL (line 14). Enforce consistent key ordering to align with linter expectations.
Apply this diff to reorder the keys:
# --- Assisted Service API ---
# Assisted Service API base URL
INVENTORY_URL=https://api.openshift.com/api/assisted-install/v2
+ # Enable verbose client logging (true/false)
+ CLIENT_DEBUG=false
# URL endpoint used to fetch the pull secret
PULL_SECRET_URL=https://api.openshift.com/api/accounts_mgmt/v1/access_token
- # Enable verbose client logging (true/false)
- CLIENT_DEBUG=false📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| INVENTORY_URL=https://api.openshift.com/api/assisted-install/v2 | |
| # URL endpoint used to fetch the pull secret | |
| PULL_SECRET_URL=https://api.openshift.com/api/accounts_mgmt/v1/access_token | |
| # Enable verbose client logging (true/false) | |
| CLIENT_DEBUG=false | |
| INVENTORY_URL=https://api.openshift.com/api/assisted-install/v2 | |
| # Enable verbose client logging (true/false) | |
| CLIENT_DEBUG=false | |
| # URL endpoint used to fetch the pull secret | |
| PULL_SECRET_URL=https://api.openshift.com/api/accounts_mgmt/v1/access_token |
🧰 Tools
🪛 dotenv-linter (3.3.0)
[warning] 18-18: [UnorderedKey] The CLIENT_DEBUG key should go before the INVENTORY_URL key
(UnorderedKey)
🤖 Prompt for AI Agents
In .env.template around lines 14 to 18, reorder the keys in the "Assisted
Service API" block so CLIENT_DEBUG appears before INVENTORY_URL: move the
CLIENT_DEBUG entry (and its comment if applicable) above the INVENTORY_URL line,
preserving the existing comments and values exactly and keeping the other keys
(PULL_SECRET_URL) in the same relative order.
| # Enable troubleshooting tool calls: 0 (disabled) | 1 (enabled) | ||
| ENABLE_TROUBLESHOOTING_TOOLS=0 |
There was a problem hiding this comment.
Align boolean representation with other variables.
ENABLE_TROUBLESHOOTING_TOOLS uses numeric 0/1 values, while CLIENT_DEBUG and LOG_TO_FILE use true/false strings. Inconsistent boolean formats may confuse users and risk type mismatches during settings validation by pydantic.
Apply this diff to align the representation:
# --- Features ---
# Enable troubleshooting tool calls: 0 (disabled) | 1 (enabled)
- ENABLE_TROUBLESHOOTING_TOOLS=0
+ ENABLE_TROUBLESHOOTING_TOOLS=falseUpdate the comment to match the new format:
- # Enable troubleshooting tool calls: 0 (disabled) | 1 (enabled)
+ # Enable troubleshooting tool calls (true/false)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Enable troubleshooting tool calls: 0 (disabled) | 1 (enabled) | |
| ENABLE_TROUBLESHOOTING_TOOLS=0 | |
| # Enable troubleshooting tool calls (true/false) | |
| ENABLE_TROUBLESHOOTING_TOOLS=false |
🤖 Prompt for AI Agents
In .env.template around lines 38 to 39, the ENABLE_TROUBLESHOOTING_TOOLS
variable uses numeric 0/1 while other booleans use "true"/"false"; update the
variable to use the same string boolean format (e.g.,
ENABLE_TROUBLESHOOTING_TOOLS=false) and change the comment to reflect true/false
usage (e.g., # Enable troubleshooting tool calls: false (disabled) | true
(enabled)) so formats are consistent for users and pydantic validation.
0969967 to
59fc620
Compare
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (2)
.env.template (2)
14-18: Key ordering issue flagged by linter (already noted).The static analysis tool continues to flag that
CLIENT_DEBUG(line 16) should appear beforeINVENTORY_URL(line 14) for proper key ordering.
38-39: Inconsistent boolean representation (already noted).
ENABLE_TROUBLESHOOTING_TOOLSuses numeric0/1while other boolean variables (CLIENT_DEBUG,LOG_TO_FILE) usetrue/falsestrings. This inconsistency may confuse users.
🧹 Nitpick comments (3)
assisted_service_mcp/src/tools/operator_tools.py (1)
41-46: Consider dynamic reference over hard-coded bundle names.The Field description hard-codes
'virtualization'and'openshift-ai'as available bundle names. While this helps guide the model, it creates a maintenance burden if new bundles are added. Consider referencinglist_operator_bundles()for the current list, or noting that the list may change.tests/test_tools_module.py (1)
523-534: Consider async test decorator for consistency.This test uses a sync function wrapper with
asyncio.run()instead of the@pytest.mark.asynciodecorator used elsewhere. While functional, using the async pattern would be more consistent with the rest of the test suite.Apply this diff to align with other tests:
-def test_list_versions_error_branch() -> None: # type: ignore[no-untyped-def] - async def run() -> None: +@pytest.mark.asyncio +async def test_list_versions_error_branch() -> None: - with patch( - "assisted_service_mcp.src.service_client.assisted_service_api.InventoryClient.get_openshift_versions", - side_effect=Exception("boom"), - ): - try: - await list_versions(lambda: "token") - except Exception as e: # noqa: BLE001 - assert "boom" in str(e) - - asyncio.run(run()) + with patch( + "assisted_service_mcp.src.service_client.assisted_service_api.InventoryClient.get_openshift_versions", + side_effect=Exception("boom"), + ): + try: + await list_versions(lambda: "token") + except Exception as e: # noqa: BLE001 + assert "boom" in str(e)assisted_service_mcp/src/tools/cluster_tools.py (1)
340-341: Move_get_cluster_infra_env_idimport to module level.No circular dependency exists between
cluster_tools.pyandshared_helpers.py. The local import on line 341 with the comment "avoid circular imports" is unnecessary. Move this import to the top-level imports (after line 11) for consistency with other imports and remove the misleading comment.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
.env.template(1 hunks)assisted_service_mcp/src/api.py(1 hunks)assisted_service_mcp/src/mcp.py(1 hunks)assisted_service_mcp/src/tools/cluster_tools.py(1 hunks)assisted_service_mcp/src/tools/download_tools.py(1 hunks)assisted_service_mcp/src/tools/event_tools.py(1 hunks)assisted_service_mcp/src/tools/host_tools.py(1 hunks)assisted_service_mcp/src/tools/network_tools.py(1 hunks)assisted_service_mcp/src/tools/operator_tools.py(1 hunks)assisted_service_mcp/src/tools/version_tools.py(1 hunks)tests/test_metrics.py(1 hunks)tests/test_tools_module.py(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
- tests/test_metrics.py
- assisted_service_mcp/src/tools/host_tools.py
- assisted_service_mcp/src/tools/network_tools.py
- assisted_service_mcp/src/tools/version_tools.py
- assisted_service_mcp/src/tools/download_tools.py
- assisted_service_mcp/src/tools/event_tools.py
- assisted_service_mcp/src/mcp.py
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: carbonin
PR: openshift-assisted/assisted-service-mcp#111
File: pyproject.toml:9-9
Timestamp: 2025-09-25T19:01:36.933Z
Learning: The `mcp` Python package (mcp>=1.15.0) includes FastMCP functionality and provides the same import path `from mcp.server.fastmcp import FastMCP` for backward compatibility with the standalone `fastmcp` package. This allows drop-in replacement when migrating from `fastmcp>=2.8.0` to `mcp>=1.15.0` without requiring code changes.
🧬 Code graph analysis (4)
assisted_service_mcp/src/tools/operator_tools.py (2)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (2)
get_operator_bundles(427-437)add_operator_bundle_to_cluster(440-467)
assisted_service_mcp/src/api.py (2)
assisted_service_mcp/src/mcp.py (1)
AssistedServiceMCPServer(27-169)assisted_service_mcp/src/logger.py (1)
configure_logging(132-178)
tests/test_tools_module.py (11)
assisted_service_mcp/src/tools/version_tools.py (1)
list_versions(12-31)assisted_service_mcp/src/tools/operator_tools.py (2)
list_operator_bundles(13-32)add_operator_bundle_to_cluster(36-81)assisted_service_mcp/src/service_client/assisted_service_api.py (16)
add_operator_bundle_to_cluster(440-467)update_cluster(347-385)create_cluster(256-295)install_cluster(388-403)get_events(173-214)list_infra_envs(235-253)get_infra_env_download_url(533-557)get_presigned_for_cluster_credentials(501-530)update_host(470-498)get_operator_bundles(427-437)get_infra_env(217-232)get_openshift_versions(406-424)create_infra_env(298-320)update_infra_env(323-344)get_cluster(117-141)list_clusters(144-154)assisted_service_mcp/src/mcp.py (1)
AssistedServiceMCPServer(27-169)assisted_service_mcp/src/tools/cluster_tools.py (7)
set_cluster_platform(247-277)set_cluster_vips(200-243)create_cluster(80-196)install_cluster(281-307)set_cluster_ssh_key(311-364)cluster_info(15-44)list_clusters(48-76)assisted_service_mcp/src/tools/event_tools.py (2)
cluster_events(12-41)host_events(45-89)assisted_service_mcp/src/tools/download_tools.py (2)
cluster_iso_download_url(46-117)cluster_credentials_download_url(121-178)assisted_service_mcp/src/tools/host_tools.py (1)
set_host_role(13-54)assisted_service_mcp/src/tools/network_tools.py (4)
validate_nmstate_yaml(22-44)alter_static_network_config_nmstate_for_host(84-146)list_static_network_config(150-180)generate_nmstate_yaml(48-80)assisted_service_mcp/src/utils/static_net/template.py (2)
NMStateTemplateParams(102-118)EthernetInterfaceParams(37-44)tests/test_utils.py (1)
create_test_cluster(10-31)
assisted_service_mcp/src/tools/cluster_tools.py (5)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (8)
InventoryClient(27-557)get_cluster(117-141)list_clusters(144-154)create_cluster(256-295)create_infra_env(298-320)update_cluster(347-385)install_cluster(388-403)update_infra_env(323-344)assisted_service_mcp/src/service_client/helpers.py (1)
Helpers(7-38)assisted_service_mcp/src/utils/log_analyzer/main.py (1)
analyze_cluster(14-71)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
🪛 dotenv-linter (3.3.0)
.env.template
[warning] 16-16: [UnorderedKey] The CLIENT_DEBUG key should go before the INVENTORY_URL key
(UnorderedKey)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Red Hat Konflux / assisted-service-mcp-saas-main-on-pull-request
|
/test eval-test |
59fc620 to
3d600ec
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (5)
.env.template (2)
12-18: Reorder keys to satisfy dotenv-linter UnorderedKey.Move CLIENT_DEBUG before INVENTORY_URL within the section.
# --- Assisted Service API --- -# Assisted Service API base URL -INVENTORY_URL=https://api.openshift.com/api/assisted-install/v2 # Enable verbose client logging (true/false) CLIENT_DEBUG=false +# Assisted Service API base URL +INVENTORY_URL=https://api.openshift.com/api/assisted-install/v2 # URL endpoint used to fetch the pull secret PULL_SECRET_URL=https://api.openshift.com/api/accounts_mgmt/v1/access_token
38-39: Align boolean format with other variables.Use true/false for ENABLE_TROUBLESHOOTING_TOOLS like CLIENT_DEBUG/LOG_TO_FILE.
-# Enable troubleshooting tool calls: 0 (disabled) | 1 (enabled) -ENABLE_TROUBLESHOOTING_TOOLS=0 +# Enable troubleshooting tool calls (true/false) +ENABLE_TROUBLESHOOTING_TOOLS=falseassisted_service_mcp/src/tools/network_tools.py (2)
43-44: Return friendly validation errors instead of propagating ValueError.Propagating raises an MCP error; return a user-readable message like other tools.
- validate_and_parse_nmstate(nmstate_yaml) - return "YAML is valid" + try: + validate_and_parse_nmstate(nmstate_yaml) + return "YAML is valid" + except ValueError as e: + log.error("Invalid NMState YAML: %s", e) + return f"ERROR: Invalid NMState YAML: {str(e)}"
170-180: Critical: static_network_config is double-encoded when already a JSON string.The Assisted Service API returns static_network_config as a JSON string in responses; json.dumps wraps it again. Handle str/list/None explicitly.
- return json.dumps(infra_envs[0].get("static_network_config", [])) + value = infra_envs[0].get("static_network_config") + if value is None: + return "[]" + if isinstance(value, str): + return value # API already returns JSON string + if isinstance(value, list): + return json.dumps(value) + log.warning("Unexpected type for static_network_config: %s", type(value).__name__) + return "[]"assisted_service_mcp/src/tools/cluster_tools.py (1)
340-342: Move helper import to top-level; no circular dependency observed.
shared_helpersdoes not importcluster_tools, so a local import is unnecessary and trips C0415. Import once at the top with other module imports.-from assisted_service_mcp.src.utils.log_analyzer.main import analyze_cluster +from assisted_service_mcp.src.utils.log_analyzer.main import analyze_cluster +from assisted_service_mcp.src.tools.shared_helpers import _get_cluster_infra_env_id @@ - # Import helper function here to avoid circular imports - from assisted_service_mcp.src.tools.shared_helpers import _get_cluster_infra_env_id
🧹 Nitpick comments (11)
assisted_service_mcp/src/tools/event_tools.py (1)
39-41: Standardize error handling across tools.Other tools tend to return user-facing "ERROR: ..." strings, while these re-raise exceptions. Pick one convention repo-wide (raise vs. return error strings) for consistent MCP responses. If choosing returns, wrap and return messages here as well; if choosing raise, align network_tools to raise.
Also applies to: 82-89
assisted_service_mcp/src/tools/network_tools.py (1)
126-135: Optional: treat '[]' (string) as empty for delete pre-check.Current check only catches None/""; an empty config from API is often "[]", so the pre-check won’t trip and downstream will raise IndexError. Consider parsing first to give a clearer error.
- if new_nmstate_yaml is None: - if index is None: + if new_nmstate_yaml is None: + if index is None: raise ValueError("index cannot be null when removing a host yaml") - if not infra_env.static_network_config: - raise ValueError( - "cannot remove host yaml with empty existing static network config" - ) + existing = infra_env.static_network_config or "[]" + if not json.loads(existing): + raise ValueError("cannot remove host yaml with empty existing static network config")assisted_service_mcp/src/mcp.py (2)
133-138: Verify header/context availability when offloading token generation to a thread.get_access_token/get_offline_token read request headers via mcp.get_context(). If FastMCP stores context in contextvars, it won’t propagate to a new thread. Capture needed headers (Authorization, OCM-Offline-Token) on the event-loop thread and pass them into the thread, or ensure get_context is thread-safe.
- async def wrapped(*args: Any, **kwargs: Any) -> Any: - # Generate token off the event loop; pass a cheap closure to tools - token = await asyncio.to_thread(self._get_access_token) + async def wrapped(*args: Any, **kwargs: Any) -> Any: + ctx = self.mcp.get_context() + auth = (ctx.request_context.request.headers.get("Authorization") if ctx and ctx.request_context and ctx.request_context.request else None) + offline = (ctx.request_context.request.headers.get("OCM-Offline-Token") if ctx and ctx.request_context and ctx.request_context.request else None) + token = await asyncio.to_thread(lambda: get_access_token( # type: ignore + self.mcp, + offline_token_func=(lambda: offline) if offline else self._get_offline_token, + ) if True else self._get_access_token) return await tool_func(lambda: token, *args, **kwargs)Alternatively, provide a get_access_token_from_headers helper and call that inside to_thread.
11-24: Import path consistency.Most modules import within assisted_service_mcp.src., but auth utilities import from assisted_service_mcp.utils.. If both are intended, consider documenting or standardizing package layout to avoid confusion.
assisted_service_mcp/src/settings.py (3)
17-21: Movewarningsimport to top-level to avoid E402/C0415.Inline module import at module scope can trip linters and is unnecessary here. Import
warningsonce at the top and use it in the except block.+import warnings from dotenv import load_dotenv from pydantic import Field, field_validator from pydantic_settings import BaseSettings, SettingsConfigDict # Load environment variables with error handling try: load_dotenv() except FileNotFoundError: # Expected when .env doesn't exist pass except Exception as e: # Log unexpected errors but don't fail - import warnings - warnings.warn(f"Failed to load .env file: {e}")
89-98: TreatOFFLINE_TOKENas a secret to reduce leakage risk.Using
strincreases the chance of accidental logging. PreferSecretStrso repr/str are redacted. This is a posture hardening change; follow-up use sites should call.get_secret_value().-from pydantic import Field, field_validator +from pydantic import Field, field_validator, SecretStr @@ - OFFLINE_TOKEN: Optional[str] = Field( + OFFLINE_TOKEN: Optional[SecretStr] = Field( default=None, json_schema_extra={ "env": "OFFLINE_TOKEN", "description": "OCM offline token for authentication", "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "sensitive": True, }, )Note: adjust any consumer code to unwrap via
.get_secret_value().
143-152: Considerboolfor feature toggle.
ENABLE_TROUBLESHOOTING_TOOLSis a 0/1 int;boolis simpler and Pydantic parses "0/1/true/false" from env.- ENABLE_TROUBLESHOOTING_TOOLS: int = Field( - default=0, - ge=0, - le=1, + ENABLE_TROUBLESHOOTING_TOOLS: bool = Field( + default=False, json_schema_extra={ "env": "ENABLE_TROUBLESHOOTING_TOOLS", "description": "Whether the troubleshooting tool call(s) should be enabled", - "example": 0, + "example": False, }, )tests/test_settings.py (2)
6-16: Make settings reload deterministic and avoid double reload.Pop the module from
sys.modulesand import once under the patched env. This reduces flakiness and speeds tests.def reload_settings_with_env(env: dict[str, str]): # type: ignore[no-untyped-def] module_name = "assisted_service_mcp.src.settings" - if module_name in sys.modules: - importlib.reload(importlib.import_module(module_name)) with pytest.MonkeyPatch().context() as mp: for k, v in env.items(): mp.setenv(k, v) - # Re-import to apply env overrides - settings_mod = importlib.import_module(module_name) - importlib.reload(settings_mod) + # Force a clean import under the patched environment + sys.modules.pop(module_name, None) + settings_mod = importlib.import_module(module_name) return settings_mod.settings
62-75: Assert specific exception types; avoid brittle message matches.Use
pydantic.ValidationErrorfor these cases and avoid matching exact error text which changes across versions.-from pydantic import ValidationError # pylint: disable=import-outside-toplevel +from pydantic import ValidationError # pylint: disable=import-outside-toplevel @@ -with pytest.raises( - ValidationError, match="Input should be 'sse' or 'streamable-http'" -): +with pytest.raises(ValidationError): @@ -def test_validate_config_invalid_port_low() -> None: - with pytest.raises(Exception): +def test_validate_config_invalid_port_low() -> None: + from pydantic import ValidationError + with pytest.raises(ValidationError): @@ -def test_validate_config_invalid_port_high() -> None: - with pytest.raises(Exception): +def test_validate_config_invalid_port_high() -> None: + from pydantic import ValidationError + with pytest.raises(ValidationError): @@ -def test_validate_config_invalid_log_level() -> None: - with pytest.raises(Exception): +def test_validate_config_invalid_log_level() -> None: + from pydantic import ValidationError + with pytest.raises(ValidationError): @@ -def test_validate_config_invalid_transport() -> None: - with pytest.raises(Exception): +def test_validate_config_invalid_transport() -> None: + from pydantic import ValidationError + with pytest.raises(ValidationError):Also applies to: 77-79
assisted_service_mcp/src/tools/cluster_tools.py (2)
152-161: Return-type consistency and clearer validation on platform selection.Returning a human message on invalid SNO/platform combination mixes error and success types. Prefer raising
ValueError(caught by MCP wrapper) or returning a structured error JSON to keep the contract predictable.- if platform: - # Check for invalid combination: single_node = true and platform is specified and not "none" - if single_node is True and platform != "none": - return "Platform must be set to 'none' for single-node clusters" + if platform: + if single_node and platform != "none": + raise ValueError("Platform must be 'none' for single‑node clusters")If the tool layer must return strings, catch and stringify at the boundary, not here.
232-243: Optional guardrail: validate VIPs applicability before update.For SNO or platform 'none'/'oci', VIPs are not applicable. Consider fetching the cluster and short‑circuiting with a clear message to prevent noop/misconfiguration API calls.
Example pattern:
c = await client.get_cluster(cluster_id)- if
c.high_availability_mode == "None"or platform in {'none','external(oci)'}: return guidance string.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (14)
.env.template(1 hunks)assisted_service_mcp/src/api.py(1 hunks)assisted_service_mcp/src/mcp.py(1 hunks)assisted_service_mcp/src/settings.py(1 hunks)assisted_service_mcp/src/tools/cluster_tools.py(1 hunks)assisted_service_mcp/src/tools/download_tools.py(1 hunks)assisted_service_mcp/src/tools/event_tools.py(1 hunks)assisted_service_mcp/src/tools/host_tools.py(1 hunks)assisted_service_mcp/src/tools/network_tools.py(1 hunks)assisted_service_mcp/src/tools/operator_tools.py(1 hunks)assisted_service_mcp/src/tools/version_tools.py(1 hunks)tests/test_metrics.py(1 hunks)tests/test_settings.py(1 hunks)tests/test_tools_module.py(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
- assisted_service_mcp/src/tools/version_tools.py
- assisted_service_mcp/src/tools/host_tools.py
- assisted_service_mcp/src/api.py
- tests/test_tools_module.py
- assisted_service_mcp/src/tools/operator_tools.py
- assisted_service_mcp/src/tools/download_tools.py
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: carbonin
PR: openshift-assisted/assisted-service-mcp#111
File: pyproject.toml:9-9
Timestamp: 2025-09-25T19:01:36.933Z
Learning: The `mcp` Python package (mcp>=1.15.0) includes FastMCP functionality and provides the same import path `from mcp.server.fastmcp import FastMCP` for backward compatibility with the standalone `fastmcp` package. This allows drop-in replacement when migrating from `fastmcp>=2.8.0` to `mcp>=1.15.0` without requiring code changes.
📚 Learning: 2025-09-09T18:51:46.598Z
Learnt from: keitwb
PR: openshift-assisted/assisted-service-mcp#91
File: service_client/static_net.py:21-36
Timestamp: 2025-09-09T18:51:46.598Z
Learning: In the assisted-service API, the static_network_config field is typed as a list when input to the API but comes back out as a string in responses. Functions processing this field from API responses should handle string inputs only.
Applied to files:
assisted_service_mcp/src/tools/network_tools.py
🧬 Code graph analysis (5)
tests/test_metrics.py (1)
assisted_service_mcp/src/metrics/metrics.py (3)
metrics(69-71)initiate_metrics(44-48)track_tool_usage(51-65)
assisted_service_mcp/src/tools/network_tools.py (5)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (4)
InventoryClient(27-557)get_infra_env(217-232)update_infra_env(323-344)list_infra_envs(235-253)assisted_service_mcp/src/utils/static_net/template.py (2)
NMStateTemplateParams(102-118)generate_nmstate_from_template(121-124)assisted_service_mcp/src/utils/static_net/config.py (3)
add_or_replace_static_host_config_yaml(41-70)remove_static_host_config_by_index(23-38)validate_and_parse_nmstate(96-108)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
assisted_service_mcp/src/tools/event_tools.py (2)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (1)
get_events(173-214)
assisted_service_mcp/src/tools/cluster_tools.py (5)
assisted_service_mcp/src/metrics/metrics.py (2)
metrics(69-71)track_tool_usage(51-65)assisted_service_mcp/src/service_client/assisted_service_api.py (8)
InventoryClient(27-557)get_cluster(117-141)list_clusters(144-154)create_cluster(256-295)create_infra_env(298-320)update_cluster(347-385)install_cluster(388-403)update_infra_env(323-344)assisted_service_mcp/src/service_client/helpers.py (1)
Helpers(7-38)assisted_service_mcp/src/utils/log_analyzer/main.py (1)
analyze_cluster(14-71)assisted_service_mcp/src/tools/shared_helpers.py (1)
_get_cluster_infra_env_id(7-41)
assisted_service_mcp/src/mcp.py (2)
assisted_service_mcp/utils/auth.py (2)
get_offline_token(10-45)get_access_token(48-117)assisted_service_mcp/src/utils/static_net/template.py (1)
NMStateTemplateParams(102-118)
🪛 dotenv-linter (3.3.0)
.env.template
[warning] 16-16: [UnorderedKey] The CLIENT_DEBUG key should go before the INVENTORY_URL key
(UnorderedKey)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Red Hat Konflux / assisted-service-mcp-saas-main-on-pull-request
🔇 Additional comments (1)
tests/test_metrics.py (1)
15-17: LGTM: robust metric assertions with unique label and exact deltas.Good use of a unique tool_name and before/after deltas; avoids REGISTRY cross-test interference and asserts exact +2 increments for both counter and histogram.
Also applies to: 44-47
There was a problem hiding this comment.
Thanks for adding this. I'll expand on these later, but it's good to have something.
carbonin
left a comment
There was a problem hiding this comment.
Approving this now. The only other comments were incredibly minor so we can fix them in a followup if we feel like it or remember.
|
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: carbonin, zszabo-rh The full list of commands accepted by this bot can be found here. The pull request process is described here DetailsNeeds approval from an approver in each of these files:
Approvers can indicate their approval by writing |
b37220e
into
openshift-assisted:master
Summary
By applying some best practices from the template MCP server implementation, this PR aims to modernize assisted-service-mcp to have modular and maintainable codebase with centralized configuration and clean, human-readable tool documentation. It preserves full backward compatibility while hopefully improving developer productivity and reliability.
Commit 1 — Source organization and modular architecture
server.pyinto a modular structure (server.pygets completely retired only in commit 3):assisted_service_mcp/src/main.py: entrypoint; metrics setup; server startassisted_service_mcp/src/api.py: FastAPI transport selection (SSE/streamable-http)assisted_service_mcp/src/mcp.py: MCP server initialization; tool registration; wrapper injectionassisted_service_mcp/src/tools/: domain-oriented tool modulesassisted_service_mcp/utils/: utilities (auth.py,helpers.py,client_factory.py)server.pyas a thin wrapper for backward test compatibility.Commit 2 — Centralized configuration (pydantic-settings) and tool description cleanup
assisted_service_mcp/src/settings.pyusingpydantic-settings+python-dotenvField(description=...)metadata; eliminated redundant Args sectionsCommit 3 — Testing, coverage, and package relocation, and everything else
metrics,static_net, andservice_clientunderassisted_service_mcp/src/client_factory.py)All tests are green, server boots cleanly, tools are registered, and client-facing behavior remains unchanged.
Summary by CodeRabbit
New Features
Documentation
Refactor
Tests
Chores