Skip to content

Comments

Template best practices#123

Merged
openshift-merge-bot[bot] merged 4 commits intoopenshift-assisted:masterfrom
zszabo-rh:template_best_practices
Oct 17, 2025
Merged

Template best practices#123
openshift-merge-bot[bot] merged 4 commits intoopenshift-assisted:masterfrom
zszabo-rh:template_best_practices

Conversation

@zszabo-rh
Copy link
Contributor

@zszabo-rh zszabo-rh commented Oct 13, 2025

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.

  • Benefits:
    • Clear modular architecture (easier to extend and test)
    • Centralized, type-safe configuration with .env support
    • Consistent, human-readable tool docs with precise Annotated argument metadata

Commit 1 — Source organization and modular architecture

  • Refactored the monolithic server.py into a modular structure (server.py gets completely retired only in commit 3):
    • assisted_service_mcp/src/main.py: entrypoint; metrics setup; server start
    • assisted_service_mcp/src/api.py: FastAPI transport selection (SSE/streamable-http)
    • assisted_service_mcp/src/mcp.py: MCP server initialization; tool registration; wrapper injection
    • assisted_service_mcp/src/tools/: domain-oriented tool modules
    • assisted_service_mcp/utils/: utilities (auth.py, helpers.py, client_factory.py)
  • Kept top-level server.py as a thin wrapper for backward test compatibility.

Commit 2 — Centralized configuration (pydantic-settings) and tool description cleanup

  • Centralized configuration: added assisted_service_mcp/src/settings.py using pydantic-settings + python-dotenv
  • Tool descriptions refactor:
    • Consolidated docstrings into human-readable narrative with:
      • Description, Examples, Prerequisites, Related tools, Returns
    • Moved all argument details to Annotated Field(description=...) metadata; eliminated redundant Args sections

Commit 3 — Testing, coverage, and package relocation, and everything else

  • Further changes in package layout and imports: relocated metrics, static_net, and service_client under assisted_service_mcp/src/
  • Cleaned up modules not needed anymore (e.g. client_factory.py)
  • Fixes due to linter errors and review findings

All tests are green, server boots cleanly, tools are registered, and client-facing behavior remains unchanged.

Summary by CodeRabbit

  • New Features

    • New MCP package with selectable transport (SSE or streamable‑HTTP), many cluster/network/host/operator tools, authentication utilities, Prometheus metrics, and a module entrypoint for running the service.
  • Documentation

    • Added .env template and updated README/startup instructions to use the module entrypoint.
  • Refactor

    • Centralized runtime settings with validation and improved logging (sensitive-data redaction); updated local/container run targets and test coverage configuration.
  • Tests

    • Expanded test coverage across settings, auth, metrics, tools, transports, and log analysis.
  • Chores

    • Removed legacy server script and consolidated startup/config paths.

@openshift-ci openshift-ci bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Oct 13, 2025
@openshift-ci
Copy link

openshift-ci bot commented Oct 13, 2025

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@openshift-ci openshift-ci bot added the size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. label Oct 13, 2025
@coderabbitai
Copy link

coderabbitai bot commented Oct 13, 2025

Walkthrough

Replaces 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 server.py and old service_client/logger.py, and changes entrypoints to run python -m assisted_service_mcp.src.main.

Changes

Cohort / File(s) Summary
Build & dev config
\Dockerfile`, `Makefile`, `README.md`, `pyproject.toml`, `pyrightconfig.json`, `.env.template``
Switches entrypoints to module (python -m assisted_service_mcp.src.main), updates dependencies (add pydantic-settings, python-dotenv, fastapi), moves types-requests to dev, adjusts pytest/coverage and typecheck ignores, and adds .env.template.
Package bootstrap & server
assisted_service_mcp core: \assisted_service_mcp/init.py`, `assisted_service_mcp/src/init.py`, `assisted_service_mcp/src/api.py`, `assisted_service_mcp/src/main.py`, `assisted_service_mcp/src/mcp.py``
New package init/version; api.py configures logging and exposes server and app; main.py provides main() with metrics and uvicorn startup; mcp.py implements AssistedServiceMCPServer, tool registration, and tool listing.
Settings & config model
\assisted_service_mcp/src/settings.py``
New Pydantic Settings model with dotenv support, settings singleton, get_setting() accessor, and validation helpers for port/transport/log level.
Logging
\assisted_service_mcp/src/logger.py``
New SensitiveFormatter, logging helpers, and configure_logging entrypoint (replaces removed legacy logger module).
Authentication utils
\assisted_service_mcp/utils/init.py`, `assisted_service_mcp/utils/auth.py``
New auth helpers get_offline_token and get_access_token (header/env precedence and SSO exchange).
Service client migration
\assisted_service_mcp/src/service_client/*``
Service client modules updated to package paths; configuration values read via get_setting; imports updated to centralized logger/metrics.
Tools (MCP endpoints)
\assisted_service_mcp/src/tools/*``
Added many async, instrumented MCP tools: cluster_tools.py, download_tools.py, event_tools.py, host_tools.py, network_tools.py, operator_tools.py, version_tools.py, plus shared_helpers.py.
Utilities & static-net templates
\assisted_service_mcp/src/utils/static_net/config.py`, `assisted_service_mcp/src/utils/static_net/template.py`, `assisted_service_mcp/src/tools/shared_helpers.py`, `assisted_service_mcp/src/utils/log_analyzer/*``
Tightened NMState validation and VLAN bounds, template rendering via model_dump(), added _get_cluster_infra_env_id, and adjusted log-analyzer import paths.
Metrics & instrumentation
\assisted_service_mcp/src/metrics/*``, tests
Adds /metrics, initiate_metrics, and track_tool_usage usage across tools; tests for metrics behavior.
Tests added/updated
\tests/*``
Added/updated tests for settings, auth, API transport, metrics, logger redaction, MCP tools, shared helpers, and log analyzer; removed legacy tests/test_server.py.
Removed / Deleted
\server.py`, `service_client/logger.py``
Deleted legacy monolithic server and old logging module; functionality migrated into new package modules.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • keitwb
  • eranco74
  • jhernand

Poem

🐰 I hopped through code and stitched a new nest,

Moved tools and tokens and made configs the best.
Metrics now hum and logs softly hide,
Tests check the burrow — everything's tied.
A rabbit-approved patch — light-footed and blessed.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.18% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive The PR title "Template best practices" is overly vague and generic. While it relates to the PR's intent of applying best practices from a template MCP server, it fails to communicate the specific nature or scope of changes to a teammate scanning commit history. The changeset includes significant refactoring—moving from a monolithic server.py to a modular architecture under assisted_service_mcp/src/, introducing centralized settings configuration, and reorganizing utilities—but none of these concrete changes are reflected in the title. The term "template best practices" is too broad and non-descriptive; someone reviewing history would not understand what was actually modified without opening the full PR description. Revise the title to be more specific and descriptive about the main change. Consider titles like "Refactor server into modular architecture under src/" or "Modularize MCP server with centralized configuration" that clearly convey the primary refactoring effort to readers scanning the commit history without requiring them to open the full PR description.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@zszabo-rh zszabo-rh force-pushed the template_best_practices branch 4 times, most recently from 6bc07da to 050b604 Compare October 13, 2025 10:32
@zszabo-rh zszabo-rh marked this pull request as ready for review October 13, 2025 10:47
@openshift-ci openshift-ci bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Oct 13, 2025
@openshift-ci openshift-ci bot requested review from eranco74 and keitwb October 13, 2025 10:47
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 requires fastapi but the package is not installed, causing ModuleNotFoundError.

This is the same dependency issue as in tests/test_metrics.py. Ensure fastapi is 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": true
assisted_service_mcp/src/utils/static_net/template.py (1)

124-124: Prefer excluding None when rendering to reduce template branching

Using 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 tests

Accessing 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-access
tests/test_api.py (1)

18-27: Consider verifying transport-specific behavior beyond attribute existence.

While the tests confirm that app and server attributes 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 True
assisted_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_TIMEOUT setting 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 getattr and 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 test

Also applies to: 45-45, 67-67, 91-91

assisted_service_mcp/src/tools/event_tools.py (1)

44-50: Inconsistent token retrieval pattern.

cluster_events retrieves the token separately (line 46) before creating the client (line 47), while host_events retrieves 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 result

Also applies to: 95-104

assisted_service_mcp/src/settings.py (1)

146-177: Remove unused validate_config in assisted_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

📥 Commits

Reviewing files that changed from the base of the PR and between 0874d98 and 050b604.

⛔ Files ignored due to path filters (1)
  • uv.lock is 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 extraPaths

 pyrightconfig.json
   "excludeTests": false,
-  "useLibraryCodeForTypes": false
+  "useLibraryCodeForTypes": true
assisted_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 platform is Optional[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 in pyproject.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-settings and python-dotenv align with the new centralized configuration system introduced in assisted_service_mcp/src/settings.py.


52-52: LGTM!

Adding integration_test to 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 layout

The 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 IndexError instead of ValueError for out-of-range index operations, aligning with the implementation changes in config.py where remove_static_host_config_by_index raises IndexError for invalid indexes.

Also applies to: 95-95

Makefile (2)

18-18: LGTM! Updated to use module-based entry point.

The run-local target now correctly invokes the new module entry point python -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_client and assisted_service_mcp packages, correctly excluding the removed server module.

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 AsyncMock and pytest.mark.asyncio is 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_mcp package
  • Update the CMD to invoke the module-based entry point python -m assisted_service_mcp.src.main

These 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_id helper 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 between set_host_role and set_cluster_ssh_key tools.

assisted_service_mcp/src/service_client/assisted_service_api.py (1)

43-44: LGTM! Centralized configuration via get_setting.

The migration from direct os.environ access to get_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 _ReqCtx and _MCP helper classes properly simulate the MCP context structure needed for testing authentication utilities. The use of types.SimpleNamespace provides a lightweight mock that matches the expected interface.


26-42: LGTM! Tests comprehensively cover offline token retrieval logic.

The three test functions thoroughly verify:

  1. Environment variable takes precedence over header value
  2. Fallback to header when environment variable is None
  3. 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 ValueError to IndexError for 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_transport helper effectively:

  1. Sets the TRANSPORT environment variable via MonkeyPatch
  2. Reloads the settings module to apply the change
  3. 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 instantiating AssistedServiceMCPServer() 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:

  1. Retrieves TRANSPORT from settings with "sse" as default
  2. Uses case-insensitive comparison
  3. Logs which transport is selected
  4. 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:

  1. Logs startup configuration details (TRANSPORT, HOST, PORT)
  2. Initializes metrics with tool names via server.list_tools_sync()
  3. Registers the /metrics endpoint before starting the server
  4. Uses uvicorn with settings-based configuration
  5. Handles KeyboardInterrupt for graceful shutdown
  6. Catches unexpected exceptions with logging and re-raise
  7. 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:

  1. Always includes the "url" field
  2. Conditionally includes "expires_at" only when it exists and is meaningful (not ZERO_DATETIME)
  3. 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:

  1. Uses @track_tool_usage() decorator for metrics
  2. Receives get_access_token_func as the first parameter for dependency injection
  3. Uses Annotated with Field descriptions for all parameters
  4. Leverages Literal type for the role parameter to restrict values

The 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:

  1. Logs the operation with relevant parameters
  2. Creates an InventoryClient with the access token
  3. Retrieves the InfraEnv ID using the shared helper _get_cluster_infra_env_id
  4. Updates the host with the specified role via client.update_host
  5. 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_client path for both InventoryClient and AssistedServiceAPIError.


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.settings instead 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_env helper properly applies environment patches within the MonkeyPatch().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-toplevel warnings 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 Annotated and Field for 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 Annotated and Field for parameter descriptions

The only concern is the inconsistent return format in cluster_iso_download_url flagged above.

tests/test_integration_api.py (1)

10-12: MonkeyPatch context concern is unfounded. importlib.reload(settings_mod) is called inside the with pytest.MonkeyPatch().context() block, so the patched TRANSPORT env 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 correct

Signature surgery and injection are clean; preserves tool doc/signature for clients.


156-168: Synchronous tool listing guard — LGTM

Properly avoids nesting event loops and falls back to asyncio.run when safe.

Comment on lines +211 to +221
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", []))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Suggested change
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.

Comment on lines +40 to +45
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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.

@zszabo-rh zszabo-rh force-pushed the template_best_practices branch from 050b604 to d4e88df Compare October 13, 2025 11:44
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Helpers
assisted_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, Optional
assisted_service_mcp/utils/auth.py (1)

3-7: Move the typing imports 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 requests
assisted_service_mcp/src/tools/network_tools.py (2)

23-23: Silence unused-argument warnings without changing contract

Rename 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 contract

Also applies to: 60-60


211-221: Fix double-encoded JSON and relax “exactly 1 infra env” requirement

The 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 paths

Errors/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 fastapi but it's not listed in the test dependencies, causing ModuleNotFoundError. Add fastapi to your test/dev requirements (e.g., pyproject.toml under test dependencies or requirements-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 True to get_openshift_versions(), which returns only latest versions. Additionally, the result may be a model object that requires .to_dict() conversion before json.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-statement
assisted_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_tools and shared_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 id while 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 surface

Use 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,
+        )
+        raise

Also applies to: 105-112

assisted_service_mcp/src/tools/download_tools.py (1)

107-112: Constrain file_name to valid choices

Enforce 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 context

Use 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 YAML

The 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._filter directly (a protected member). While this is functional, consider adding a public filter_text() method to SensitiveFormatter or 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-access
tests/test_api.py (1)

6-15: Consider test isolation for module reloads.

The import_api_with_transport helper 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

📥 Commits

Reviewing files that changed from the base of the PR and between 050b604 and d4e88df.

⛔ Files ignored due to path filters (1)
  • uv.lock is 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 AsyncMock is 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 Annotated and Field
  • 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.settings attributes 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 AsyncMock for async methods and the mock object construction with type() for to_str() methods is appropriate.


106-127: LGTM!

The test correctly validates that the cluster_events tool 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.raises with match for 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_bundles function correctly handles the response. The InventoryClient.get_operator_bundles() method already converts model objects to dictionaries, so json.dumps() will work properly.


127-133: Confirm models.Cluster implements to_str()
Ensure models.Cluster defines or inherits a to_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 SensitiveFormatter correctly 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_cluster for single-node platform combinations and the partial failure handling in set_cluster_ssh_key demonstrate thoughtful implementation.

"integration_test/performance"
],
"excludeTests": false,
"useLibraryCodeForTypes": false
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
"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.

Copy link
Collaborator

@carbonin carbonin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this in examples?

Also why is Examples necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't say the operator versions tools are really related to "versions"

@zszabo-rh
Copy link
Contributor Author

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.

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.

@zszabo-rh zszabo-rh force-pushed the template_best_practices branch from d4e88df to fb1108a Compare October 14, 2025 07:34
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_LEVEL might 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.INFO
tests/test_metrics.py (1)

1-45: Missing FastAPI dependency causes test failures.

The test imports fastapi and fastapi.testclient but the package is not declared in test or dev dependencies, causing ModuleNotFoundError in the pipeline.

Add fastapi to your test dependencies in pyproject.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, and assisted_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_setting
assisted_service_mcp/src/settings.py (1)

147-177: Avoid shadowing the module-level settings instance.

The validate_config parameter name conflicts with the global settings, triggering redefined-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_func to _ 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_func to _ 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.dumps wraps it again producing a JSON string literal, not an array. The retrieved learning confirms that static_network_config is 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.logger by the service_client package. While the current structure works, as the codebase grows you might consider whether a top-level assisted_service_mcp.logger or assisted_service_mcp.common.logger would 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 pytest
tests/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

📥 Commits

Reviewing files that changed from the base of the PR and between d4e88df and fb1108a.

⛔ Files ignored due to path filters (1)
  • uv.lock is 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.0 and python-dotenv>=1.0.0 correctly 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-toplevel is justified given the intentional use of function-scoped imports in logger.py and settings.py to 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.helpers to assisted_service_mcp.src.service_client.helpers correctly 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.py to uv run python -m assisted_service_mcp.src.main correctly 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_mcp ensures the new package modules are included in coverage reports alongside the existing service_client coverage. 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 AsyncMock is 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.main correctly 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_id helper provides clear error messages for edge cases (no InfraEnvs, multiple InfraEnvs, missing ID) and includes informative logging. The implementation correctly handles the return type from list_infra_envs and 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 app and server attributes 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 (_ReqCtx and _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 before uvicorn.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 BaseSettings and Field with json_schema_extra makes 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_tool method elegantly solves the token injection problem by:

  • Wrapping the tool function to inject _get_access_token as the first parameter
  • Removing the injected parameter from the exposed signature via inspect.signature
  • Preserving function metadata with @wraps

This 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_sync method 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 Exception catch 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_func to 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.

@zszabo-rh zszabo-rh force-pushed the template_best_practices branch from fb1108a to 3a2c8d9 Compare October 14, 2025 07:54
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 use getattr(settings, "LOGGING_LEVEL", None) with isinstance checks 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-statement
assisted_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:

  1. Passing True to get_openshift_versions returns only latest versions, contradicting the docstring's claim of a "complete list"
  2. The result is a models.OpenshiftVersions object 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 fastapi is not in test dependencies, causing ModuleNotFoundError during 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.asyncio and defined as async def, but none of them contain await statements 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 if configure_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 pattern

In cluster_events (lines 46-47), the access token is retrieved and assigned to a variable before instantiating InventoryClient, 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.modules and 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_route helper (lines 21-36) adds the /metrics route because it's normally added in main(). The complex route path extraction (lines 27-34) with nested getattr/hasattr calls 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:

  1. Adding a dedicated /health or /liveness endpoint that always returns 200
  2. 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_id to the module’s top-level imports in assisted_service_mcp/src/tools/cluster_tools.py and remove the “avoid circular imports” comment, as no circular dependency exists.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fb1108a and 3a2c8d9.

⛔ Files ignored due to path filters (1)
  • uv.lock is 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=2 ensures 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_func parameter 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) >= 1 guard 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 RuntimeError from nested asyncio.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 /metrics endpoint 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 getattr with a default value for openshift_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 True check 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.

@zszabo-rh zszabo-rh force-pushed the template_best_practices branch from 3a2c8d9 to bea136b Compare October 14, 2025 09:11
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (7)
tests/test_logger_filter.py (2)

17-23: Same issue as noted in test_redact_object_style_single_quotes: consider verifying that original sensitive values are not present in the redacted output.


26-32: Same issue as noted in test_redact_object_style_single_quotes: consider verifying that original sensitive values are not present in the redacted output.

pyrightconfig.json (1)

12-12: Re-enable useLibraryCodeForTypes for 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 as Unknown, hiding potential type errors in integrations.

As noted in the previous review, please set this to true or remove the property:

-  "useLibraryCodeForTypes": false
+  "useLibraryCodeForTypes": true
assisted_service_mcp/src/settings.py (1)

147-177: Rename parameter to avoid shadowing the global settings.

The function parameter name settings shadows the module-level settings instance (line 181), triggering the redefined-outer-name warning (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_url function 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 redacted

Apply 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, the SensitiveFormatter._filter method also handles dictionary-style patterns (e.g., '_pull_secret': 'value') according to the implementation in assisted_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 redacted
tests/test_api.py (2)

18-21: Consider verifying transport-specific configuration.

The test validates that app and server attributes exist, but doesn't verify they're configured for SSE. You could enhance this by checking server.mcp.stateless_http is 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_http is 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 True
tests/test_integration_api.py (1)

21-36: Consider simplifying the route lookup.

The defensive route lookup with multiple getattr calls 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

📥 Commits

Reviewing files that changed from the base of the PR and between 3a2c8d9 and bea136b.

⛔ Files ignored due to path filters (1)
  • uv.lock is 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__.py with 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 _filter method. Using protected-access in 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_mcp package structure, replacing the old server package coverage.

pyproject.toml (4)

17-18: LGTM! Settings dependencies added.

The addition of pydantic-settings>=2.6.0 and python-dotenv>=1.0.0 supports the new centralized configuration module introduced in this PR.


30-31: LGTM! Type stubs and dev dependencies properly configured.

The types-requests dependency is now correctly placed in dev dependencies only (not duplicated in main), and fastapi is added to support the new API transport layer.


38-38: LGTM! FastAPI added to test dependencies.

This resolves the pipeline failure where test_metrics.py couldn't import fastapi.testclient.


53-64: LGTM! Pylint configuration updated appropriately.

The addition of integration_test to ignore-paths and import-outside-toplevel to 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 /metrics endpoint returns Prometheus-formatted output with HTTP 200.


18-45: LGTM! Comprehensive decorator test.

The test validates both counter and histogram metrics, with the >= 2.0 threshold correctly accounting for the initiate_metrics initialization 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_client paths, 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.settings path 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 .env files 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_setting function'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_id returns the ID from the first infrastructure environment.


15-20: LGTM! Proper error handling for empty list.

The test verifies that a ValueError is raised when no infrastructure environments are found.


23-28: LGTM! Edge case for missing ID field.

The test ensures that a ValueError is 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 in for 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_yaml with 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: ignore for __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.

@zszabo-rh zszabo-rh force-pushed the template_best_practices branch from bea136b to 7017285 Compare October 14, 2025 09:40
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Line 40 passes True (only latest) but the docstring promises "complete list of OpenShift versions"
  2. Line 42 may fail if result is not JSON-serializable (it's a models.OpenshiftVersions object)

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_url returns 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 (from shared_helpers.py lines 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 one
assisted_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_id to module level
The function-level import isn’t needed—relocate it alongside the other imports at the top of cluster_tools.py.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bea136b and 7017285.

⛔ Files ignored due to path filters (1)
  • uv.lock is 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 ValueError to IndexError aligns with Python conventions for index-related errors. Note this is a breaking change if callers were catching ValueError specifically.


76-77: LGTM: Explicit validation improves error messaging.

Adding an explicit check for interfaces is None provides a clear error message rather than letting it fail with a TypeError during iteration.

assisted_service_mcp/src/utils/static_net/template.py (3)

66-69: LGTM: Correct validation for bond interfaces.

Adding min_length=2 correctly 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=4094 correctly 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.logger aligns 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_bundles implementation correctly handles JSON serialization since get_operator_bundles() already returns a list of dicts.


85-141: LGTM!

The add_operator_bundle_to_cluster implementation 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 (app and server) for both transport configurations.


6-15: Verify test isolation across test order

Ensure import_api_with_transport doesn’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
Comment on lines 33 to 34
uv run --group test pytest --cov=service_client --cov=assisted_service_mcp --cov-report=html --cov-report=term-missing

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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-missing

As 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.

@zszabo-rh zszabo-rh force-pushed the template_best_practices branch 2 times, most recently from 700a108 to 68d881d Compare October 14, 2025 10:30
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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": true
tests/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:

  1. Uses the literal "foo_tool" name which can collide with other tests
  2. Asserts >= 2.0 instead of exact equality
  3. 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 logger

Assigning 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 paths

cluster_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 payload

Return 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_config

API 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 narrowly

Disabling 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_DATETIME as 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_str

However, 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_events function 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 node
tests/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-toplevel

If 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 import

Support 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

📥 Commits

Reviewing files that changed from the base of the PR and between 7017285 and 68d881d.

⛔ Files ignored due to path filters (1)
  • uv.lock is 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 runtime

pydantic-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 tests

Ensure 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 /metrics endpoint 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_events function 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_id helper function:

  • Success case with valid InfraEnv
  • Error case when no InfraEnvs exist
  • Error case when InfraEnv lacks a valid ID

The use of AsyncMock is 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 IndexError for out-of-range operations, aligning with the changes in assisted_service_mcp/src/utils/static_net/config.py where ValueError was changed to IndexError for index validation failures.

assisted_service_mcp/src/tools/shared_helpers.py (1)

7-41: LGTM!

The _get_cluster_infra_env_id helper 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 ValueError to IndexError for 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 interfaces key exists in the nmstate YAML
  • Line 84: Requires both mac-address and name to be present, ensuring only complete interface definitions are processed

These 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 that assisted_service_mcp/src/metrics/__init__.py explicitly re-exports track_tool_usage, making the current import valid.

@zszabo-rh zszabo-rh force-pushed the template_best_practices branch 2 times, most recently from 2803649 to 12b9b3e Compare October 14, 2025 11:36
@zszabo-rh zszabo-rh force-pushed the template_best_practices branch from 283bd45 to 6dcc6ee Compare October 15, 2025 11:46
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 uses uv run python -m assisted_service_mcp.src.main. The README command will fail because uv run requires explicit python -m to 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 signature

The 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 import

Current 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 None and ZERO_DATETIME cases for expires_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 traces

Use 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
+        )
+        raise

Also 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.logger exports a module-level log at 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 leakage

Use 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 handlers

Strengthen 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 string

Returning 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 alternative

Current 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 consistency

Mark 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 stability

Patch 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 AttributeError

Avoid 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 overhead

Repeated 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 helper

The 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 patching

Tool 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 import

Avoid 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 propagation

Without 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 = False

Also applies to: 162-173


93-96: Optional: set third-party log levels in one place

You 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 import

No 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_id

Also applies to: 389-391

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 283bd45 and 6dcc6ee.

⛔ Files ignored due to path filters (1)
  • uv.lock is 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 -m to run the module entry point, following Python best practices for package execution.

Makefile (2)

18-18: LGTM!

The run-local target correctly uses python -m for module invocation, consistent with the Dockerfile approach.


33-33: LGTM!

Using --cov=assisted_service_mcp is 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 schema

Implementation looks good. Ensure registration wraps this function so get_access_token_func is not exposed as a tool argument to clients.

tests/test_assisted_service_api.py (2)

12-13: Import path updates align with new layout

New imports from assisted_service_mcp.src.service_client.* look correct.


141-141: ApiClient patch target is correct

Patching ApiClient in the module under test ensures the constructor interception works.

tests/test_service_client_api.py (1)

13-21: Patch targets look correct

Patching get_setting and Configuration at the module-under-test level is appropriate and keeps the test hermetic.

@zszabo-rh zszabo-rh force-pushed the template_best_practices branch from 6dcc6ee to 227b497 Compare October 15, 2025 12:11
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) returns None for empty strings or YAML null values (via yaml.safe_load). Line 75 calls .get("interfaces") on that result, which raises AttributeError when nmstate is None or 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 = False

Also 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.dumps wraps 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_id should be relocated to the module's top-level imports. Past verification confirmed no circular dependency exists between cluster_tools and shared_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-dev

Also 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,
+        )
+        raise

Also 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6dcc6ee and 227b497.

⛔ Files ignored due to path filters (1)
  • uv.lock is 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 platform parameter accepts Optional[str] and defaults to baremetal when None, 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_analysis

The 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 ValueError to IndexError for 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 in name_and_mac_list and aligns with the MacInterfaceMap TypedDict 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_role function 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-settings and python-dotenv enable the centralized configuration module
  • fastapi is properly placed in main dependencies for runtime use
  • types-requests correctly 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_archive helper provides a clean test fixture with proper handling of the unused kwargs parameter (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, and install_cluster are well-implemented with proper error handling, logging, and validation.


416-426: LGTM!

The analyze_cluster_logs function is straightforward and correctly delegates to the log analyzer utility.

Comment on lines +48 to +49
validate_and_parse_nmstate(nmstate_yaml)
return "YAML is valid"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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".

Copy link
Collaborator

@carbonin carbonin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

server = AssistedServiceMCPServer()

# Choose the appropriate transport protocol based on settings
TRANSPORT_VALUE = getattr(settings, "TRANSPORT", "sse")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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."
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not typically. Always

- 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this here?

with TestClient(app) as client:
resp = client.get("/metrics")
assert resp.status_code == 200
assert "# HELP" in resp.text or "HELP" in resp.text
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this duplicating the test test_metrics_endpoint_present_and_exposes_prometheus from test_apy.py?

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 failures

Return 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 raising

Wrap 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 handling

API 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 path

Return 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 scope

Cleaning 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_metrics multiple 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_metrics call 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_value is a helper that extracts a specific metric value for a given tool label.

.env.template (3)

35-35: Default file logging to false for containers

Writing 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 conventions

Other 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 order

Reorder 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 docs

Enumerating '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 UX

Pre-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 tests

Most 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 message

Invalid 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 one

Current 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 errors

Guard 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 string

Clarify 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

📥 Commits

Reviewing files that changed from the base of the PR and between 227b497 and a232afa.

📒 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 that track_tool_usage uses func.__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_metrics in test_apy.py, so no overlap exists. These are unit tests for the decorator and initiate_metrics; they complement (not duplicate) any HTTP /metrics endpoint tests.

assisted_service_mcp/src/tools/host_tools.py (1)

51-53: Verify update parameter name: role vs host_role

InventoryClient.update_host builds HostUpdateParams(**update_params). Many Assisted Service SDKs expect role, not host_role. If the model doesn’t accept host_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 flow

Token fetch, client calls, logging, and error propagation are clean.


44-89: LGTM for host-specific events

Consistent with cluster_events and InventoryClient API.

assisted_service_mcp/src/mcp.py (1)

121-151: Wrapper and signature shaping look solid

Offloading token generation avoids blocking, and signature trimming keeps tool surfaces clean in MCP.

@zszabo-rh zszabo-rh force-pushed the template_best_practices branch from a232afa to 731d4a1 Compare October 16, 2025 11:40
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_config as a string in responses but accepts it as a list on input. Calling json.dumps on 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_id is still inside the function despite a previous review confirming no circular dependency exists (the verification script found zero imports from cluster_tools in shared_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_id

Then 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_nmstate raises ValueError for 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

📥 Commits

Reviewing files that changed from the base of the PR and between a232afa and 731d4a1.

📒 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_events and host_events follow 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 use json.dumps({"error": ...}), and success at line 178 uses json.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"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are we using this for? Are we going to ever change it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is coming from the template but I don't really have plans for this
we'd better remove then?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah if we don't have a use for it then let's just remove it.

"""Initialize the MCP server with assisted service tools."""
try:
# Get transport configuration from settings
use_stateless_http = (settings.TRANSPORT or "").lower() == "streamable-http"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's something we need to do for this PR I was just thinking for the future.

@zszabo-rh zszabo-rh force-pushed the template_best_practices branch from 731d4a1 to 9485aaa Compare October 17, 2025 07:29
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_nmstate can raise ValueError for 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.dumps wraps 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_url returns 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_url consistently 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 diagnostics

Other 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
+        )
+        raise
assisted_service_mcp/src/tools/operator_tools.py (2)

3-6: Harden JSON serialization and capture tracebacks

Ensure 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
+        )
+        raise

Also applies to: 24-31, 74-81


41-46: Optional: restrict bundle names via Literal for better UX/validation

Enforce 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 tracebacks

Ensure 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
+        )
+        raise

Also applies to: 32-41, 72-89

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 731d4a1 and 9485aaa.

📒 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 loop

Offloading 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 bool

If 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_id

And 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 key

Verify no circular dependency exists:

Comment on lines +3 to +5
import json
from typing import Callable

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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")
+        raise

Also 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.

@zszabo-rh zszabo-rh force-pushed the template_best_practices branch from 9485aaa to 0969967 Compare October 17, 2025 08:04
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 message

validate_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 robustly

The 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/logging

json.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")
         raise
assisted_service_mcp/src/tools/download_tools.py (1)

69-117: Inconsistent return format breaks JSON parsing expectations.

cluster_iso_download_url returns 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 returns

The 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 handling

Index/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 TRANSPORT

settings.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_tools and operator_tools at the top level (lines 7-11), while other test functions import their respective tool modules locally (e.g., cluster_tools at line 16, event_tools at 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:

  1. 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:
  2. The asyncio.run(run()) pattern is fine for synchronous test functions wrapping async logic, but many other tests use @pytest.mark.asyncio directly. Consider using that pattern consistently for uniformity.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9485aaa and 0969967.

📒 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.template keys, types, defaults, and optionality correctly match the Settings model in assisted_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 solid

Logging configured early, server initialized once, and transport selection matches settings. LGTM.

tests/test_metrics.py (2)

7-47: Deterministic metrics assertions with unique labels

Good isolation from global REGISTRY and exact delta assertions. This prevents cross-test interference.


50-74: Idempotence coverage is adequate

Validates 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 signatures

Offloading 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_url returns JSON-formatted errors (lines 698-700). However, note that cluster_iso_download_url still returns plain strings for errors (tested elsewhere), creating an inconsistency that was flagged in past reviews of download_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_url helper 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_func
assisted_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 like openshift_version with a sensible default of "Unknown". This matches the API contract where client.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_node flag (lines 158-160)
  • Checks for cluster.id presence 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_cluster utility and formats results as a human-readable report. The function appropriately returns an empty string when no issues are found.

Comment on lines 14 to 18
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines +38 to +39
# Enable troubleshooting tool calls: 0 (disabled) | 1 (enabled)
ENABLE_TROUBLESHOOTING_TOOLS=0
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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=false

Update 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.

Suggested change
# 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.

@zszabo-rh zszabo-rh force-pushed the template_best_practices branch from 0969967 to 59fc620 Compare October 17, 2025 10:11
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 before INVENTORY_URL (line 14) for proper key ordering.


38-39: Inconsistent boolean representation (already noted).

ENABLE_TROUBLESHOOTING_TOOLS uses numeric 0/1 while other boolean variables (CLIENT_DEBUG, LOG_TO_FILE) use true/false strings. 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 referencing list_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.asyncio decorator 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_id import to module level.

No circular dependency exists between cluster_tools.py and shared_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

📥 Commits

Reviewing files that changed from the base of the PR and between 0969967 and 59fc620.

📒 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

@zszabo-rh
Copy link
Contributor Author

/test eval-test

@zszabo-rh zszabo-rh force-pushed the template_best_practices branch from 59fc620 to 3d600ec Compare October 17, 2025 12:44
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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=false
assisted_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_helpers does not import cluster_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: Move warnings import to top-level to avoid E402/C0415.

Inline module import at module scope can trip linters and is unnecessary here. Import warnings once 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: Treat OFFLINE_TOKEN as a secret to reduce leakage risk.

Using str increases the chance of accidental logging. Prefer SecretStr so 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: Consider bool for feature toggle.

ENABLE_TROUBLESHOOTING_TOOLS is a 0/1 int; bool is 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.modules and 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.ValidationError for 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

📥 Commits

Reviewing files that changed from the base of the PR and between 59fc620 and 3d600ec.

📒 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

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding this. I'll expand on these later, but it's good to have something.

Copy link
Collaborator

@carbonin carbonin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@openshift-ci openshift-ci bot added the lgtm Indicates that a PR is ready to be merged. label Oct 17, 2025
@openshift-ci
Copy link

openshift-ci bot commented Oct 17, 2025

[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

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci openshift-ci bot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Oct 17, 2025
@openshift-merge-bot openshift-merge-bot bot merged commit b37220e into openshift-assisted:master Oct 17, 2025
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. lgtm Indicates that a PR is ready to be merged. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants