Skip to content

test: add MCP server catalog integration tests#1168

Merged
dbasunag merged 4 commits intoopendatahub-io:mainfrom
fege:mcp_load
Mar 5, 2026
Merged

test: add MCP server catalog integration tests#1168
dbasunag merged 4 commits intoopendatahub-io:mainfrom
fege:mcp_load

Conversation

@fege
Copy link
Copy Markdown
Contributor

@fege fege commented Mar 4, 2026

Pull Request

Summary

Tests for MCP server catalog functionality:

  • Data integrity validation for YAML-sourced MCP servers
  • Multi-source catalog configuration testing
  • Error handling for malformed YAML and missing required fields
  • Verification of server metadata, tools, and custom properties

Related Issues

  • Fixes:
  • JIRA: RHOAIENG-51582

How it has been tested

  • Locally
  • Jenkins

Additional Requirements

  • If this PR introduces a new test image, did you create a PR to mirror it in disconnected environment?
  • If this PR introduces new marker(s)/adds a new component, was relevant ticket created to update relevant Jenkins job?

Summary by CodeRabbit

  • Tests

    • Added comprehensive MCP server catalog test coverage with new fixtures, test data, and suites validating data integrity, multi-source loading, and graceful handling of invalid/malformed sources.
  • Chores

    • Reorganized test utilities and imports and introduced HTTP and pod-readiness helpers to improve reliability and stability of catalog-related tests.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 4, 2026

The following are automatically added/executed:

  • PR size label.
  • Run pre-commit
  • Run tox
  • Add PR author as the PR assignee
  • Build image based on the PR

Available user actions:

  • To mark a PR as WIP, add /wip in a comment. To remove it from the PR comment /wip cancel to the PR.
  • To block merging of a PR, add /hold in a comment. To un-block merging of PR comment /hold cancel.
  • To mark a PR as approved, add /lgtm in a comment. To remove, add /lgtm cancel.
    lgtm label removed on each new commit push.
  • To mark PR as verified comment /verified to the PR, to un-verify comment /verified cancel to the PR.
    verified label removed on each new commit push.
  • To Cherry-pick a merged PR /cherry-pick <target_branch_name> to the PR. If <target_branch_name> is valid,
    and the current PR is merged, a cherry-picked PR would be created and linked to the current PR.
  • To build and push image to quay, add /build-push-pr-image in a comment. This would create an image with tag
    pr-<pr_number> to quay repository. This image tag, however would be deleted on PR merge or close action.
Supported labels

{'/hold', '/verified', '/cherry-pick', '/build-push-pr-image', '/wip', '/lgtm'}

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 4, 2026

📝 Walkthrough

Walkthrough

Adds MCP server test resources and fixtures, new MCP server constants and tests (single/multi/invalid sources), introduces shared HTTP and pod utilities in tests/model_registry/utils.py, removes duplicate helpers from model_catalog/utils.py, and updates import paths across many model_registry tests.

Changes

Cohort / File(s) Summary
Top-level conftest & route fixture
tests/model_registry/conftest.py
Added import ocp_resources.route.Route and a model_catalog_routes fixture returning model-catalog Route objects filtered by namespace and label.
MCP servers fixtures
tests/model_registry/mcp_servers/conftest.py
New class-scoped fixtures and helpers: mcp_catalog_rest_urls, wait_for_mcp_catalog_api, mcp_servers_configmap_patch, mcp_multi_source_configmap_patch, mcp_invalid_yaml_configmap_patch — build MCP REST URLs, wait for MCP API readiness with retries, and patch ConfigMaps for single/multi/invalid catalog scenarios.
MCP constants
tests/model_registry/mcp_servers/constants.py
New comprehensive constants: catalog/source IDs & names, API and YAML paths, two valid catalog YAMLs, invalid/malformed YAML examples, source dicts, and expected mappings/sets used by tests (names, tools, providers, timestamps, custom properties, source ID map).
MCP tests
tests/model_registry/mcp_servers/test_data_integrity.py, tests/model_registry/mcp_servers/test_invalid_yaml.py, tests/model_registry/mcp_servers/test_multi_source.py
Added tests validating MCP server data integrity, multi-source loading and source_id tagging, and behavior when secondary sources provide invalid YAML; includes xfail-marked cases and log assertions.
Shared test utilities (new/expanded)
tests/model_registry/utils.py
Introduced TransientUnauthorizedError, execute_get_call, execute_get_command (HTTP helpers with retry/401 handling and JSON parsing), and pod lifecycle helpers wait_for_model_catalog_pod_created and wait_for_model_catalog_pod_ready_after_deletion.
Model-catalog utils cleaned up
tests/model_registry/model_catalog/utils.py
Removed local implementations of HTTP helpers and pod-ready helpers; now import shared helpers from tests.model_registry.utils. Kept wait_for_model_catalog_api API surface.
Conftest & import refactors
tests/model_registry/model_catalog/conftest.py
Removed model_catalog_routes fixture from this conftest and updated imports to use helpers from tests.model_registry.utils.
Import-path updates across tests
tests/model_registry/model_catalog/...
(e.g. catalog_config/*, huggingface/*, metadata/*, search/*, sorting/*, upgrade/*)
Refactored many test files to import execute_get_command, pod helpers, and other utilities from tests.model_registry.utils instead of tests.model_registry.model_catalog.utils; consolidated import lines and adjusted a few utility import sources.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'test: add MCP server catalog integration tests' clearly and accurately summarizes the main change—adding new integration tests for MCP server catalogs.
Description check ✅ Passed The PR description follows the template structure with a Summary, Related Issues, and How it has been tested sections. The Summary explains the test additions, JIRA reference is provided, and test status is indicated. Only the Additional Requirements checkboxes are uncompleted, which is acceptable for non-image-related changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

Copy link
Copy Markdown
Contributor

@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

🧹 Nitpick comments (2)
tests/model_registry/mcp_servers/test_multi_source.py (1)

42-47: Prefer explicit assertion over KeyError for unknown server names.

At Line 44, direct indexing can fail with an opaque traceback. A guarded lookup keeps failure output clearer.

Proposed refinement
         for server in response.get("items", []):
             name = server["name"]
-            expected_source = EXPECTED_MCP_SOURCE_ID_MAP[name]
+            expected_source = EXPECTED_MCP_SOURCE_ID_MAP.get(name)
+            assert expected_source is not None, f"Unexpected MCP server returned: {name}"
             assert server.get("source_id") == expected_source, (
                 f"Server '{name}' has source_id '{server.get('source_id')}', expected '{expected_source}'"
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/model_registry/mcp_servers/test_multi_source.py` around lines 42 - 47,
The test currently does a direct lookup EXPECTED_MCP_SOURCE_ID_MAP[name] which
will raise a KeyError for unknown server names; change this to a guarded lookup
and explicit assertion: first assert that name is present in
EXPECTED_MCP_SOURCE_ID_MAP (use "name in EXPECTED_MCP_SOURCE_ID_MAP") with a
clear failure message, then retrieve expected_source via
EXPECTED_MCP_SOURCE_ID_MAP.get(name) and assert server.get("source_id") ==
expected_source with the existing message; update the block using the variables
response, server, name, and EXPECTED_MCP_SOURCE_ID_MAP to make failures explicit
and readable.
tests/model_registry/mcp_servers/conftest.py (1)

75-78: De-duplicate mcp_catalogs entries before patching.

Current append/extend logic can duplicate source IDs when they already exist in sources.yaml, which can make assertions non-deterministic.

Proposed de-duplication helper
+def _merge_mcp_sources(current_data: dict, new_sources: list[dict]) -> None:
+    existing = {
+        source.get("id"): source
+        for source in current_data.get("mcp_catalogs", [])
+        if isinstance(source, dict) and source.get("id")
+    }
+    for source in new_sources:
+        existing[source["id"]] = source
+    current_data["mcp_catalogs"] = list(existing.values())
-    if "mcp_catalogs" not in current_data:
-        current_data["mcp_catalogs"] = []
-    current_data["mcp_catalogs"].append(MCP_CATALOG_SOURCE)
+    _merge_mcp_sources(current_data=current_data, new_sources=[MCP_CATALOG_SOURCE])
-    if "mcp_catalogs" not in current_data:
-        current_data["mcp_catalogs"] = []
-    current_data["mcp_catalogs"].extend([MCP_CATALOG_SOURCE, MCP_CATALOG_SOURCE2])
+    _merge_mcp_sources(current_data=current_data, new_sources=[MCP_CATALOG_SOURCE, MCP_CATALOG_SOURCE2])
-    if "mcp_catalogs" not in current_data:
-        current_data["mcp_catalogs"] = []
-    current_data["mcp_catalogs"].extend([MCP_CATALOG_SOURCE, MCP_CATALOG_INVALID_SOURCE])
+    _merge_mcp_sources(current_data=current_data, new_sources=[MCP_CATALOG_SOURCE, MCP_CATALOG_INVALID_SOURCE])

Also applies to: 116-119, 159-162

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/model_registry/mcp_servers/conftest.py` around lines 75 - 78, The test
setup unconditionally appends MCP_CATALOG_SOURCE to
current_data["mcp_catalogs"], causing duplicate entries; change the logic so you
deduplicate before patching—e.g., compute a unique list (by source id or full
entry) for current_data["mcp_catalogs"] and only add MCP_CATALOG_SOURCE if it
isn't already present, or replace the list with a deduped union (use a helper
like dedupe_sources(current_data["mcp_catalogs"], MCP_CATALOG_SOURCE)); apply
the same deduplication change to the other similar blocks that manipulate
current_data["mcp_catalogs"] (the sections around the other append/extend
usages).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/model_registry/mcp_servers/conftest.py`:
- Around line 44-51: The readiness check in wait_for_mcp_catalog_api only
ensures items is non-empty and can pass before all fixtures are indexed; change
the function signature to accept readiness criteria (e.g., expected_source_ids:
list[str] and/or min_server_count: int), and update the body to validate that
every expected_source_id appears in response.json()["items"] (or that len(items)
>= min_server_count) before returning, raising ResourceNotFoundError if criteria
are not yet met; update all callers to pass the appropriate expected_source_ids
or min_server_count for their fixtures and improve the error message to include
which IDs/count are still missing.
- Around line 48-50: The call to response.json() can raise
json.JSONDecodeError/ValueError and currently escapes the `@retry` handling; wrap
the response.json() call in a try/except that catches json.JSONDecodeError and
ValueError (import JSONDecodeError from json if needed) and either re-raise a
known retryable exception used by the existing `@retry` decorator or add
JSONDecodeError/ValueError to the decorator's exceptions_dict so the fixture
(the code around response.json() and the ResourceNotFoundError) will be retried
on transient non-JSON responses.

In `@tests/model_registry/mcp_servers/test_invalid_yaml.py`:
- Around line 37-39: The test currently only asserts EXPECTED_MCP_SERVER_NAMES
<= server_names which allows bad entries to slip in; update the test to also
assert negative conditions that invalid names are not present in server_names
(e.g., assert "" not in server_names, assert None not in server_names, and
assert any known malformed sentinel name used in fixtures/configs—such as a
MALFORMED_SOURCE_NAME or similar sentinel— not in server_names). Apply the same
negative-path assertions at the other check block that mirrors lines 63-65 so
both positive and explicit rejection checks run.

---

Nitpick comments:
In `@tests/model_registry/mcp_servers/conftest.py`:
- Around line 75-78: The test setup unconditionally appends MCP_CATALOG_SOURCE
to current_data["mcp_catalogs"], causing duplicate entries; change the logic so
you deduplicate before patching—e.g., compute a unique list (by source id or
full entry) for current_data["mcp_catalogs"] and only add MCP_CATALOG_SOURCE if
it isn't already present, or replace the list with a deduped union (use a helper
like dedupe_sources(current_data["mcp_catalogs"], MCP_CATALOG_SOURCE)); apply
the same deduplication change to the other similar blocks that manipulate
current_data["mcp_catalogs"] (the sections around the other append/extend
usages).

In `@tests/model_registry/mcp_servers/test_multi_source.py`:
- Around line 42-47: The test currently does a direct lookup
EXPECTED_MCP_SOURCE_ID_MAP[name] which will raise a KeyError for unknown server
names; change this to a guarded lookup and explicit assertion: first assert that
name is present in EXPECTED_MCP_SOURCE_ID_MAP (use "name in
EXPECTED_MCP_SOURCE_ID_MAP") with a clear failure message, then retrieve
expected_source via EXPECTED_MCP_SOURCE_ID_MAP.get(name) and assert
server.get("source_id") == expected_source with the existing message; update the
block using the variables response, server, name, and EXPECTED_MCP_SOURCE_ID_MAP
to make failures explicit and readable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 46d81832-e7f4-4e29-acf2-8aa729a24380

📥 Commits

Reviewing files that changed from the base of the PR and between 39d3462 and 7ec0033.

📒 Files selected for processing (24)
  • tests/model_registry/conftest.py
  • tests/model_registry/mcp_servers/__init__.py
  • tests/model_registry/mcp_servers/conftest.py
  • tests/model_registry/mcp_servers/constants.py
  • tests/model_registry/mcp_servers/test_data_integrity.py
  • tests/model_registry/mcp_servers/test_invalid_yaml.py
  • tests/model_registry/mcp_servers/test_multi_source.py
  • tests/model_registry/model_catalog/catalog_config/conftest.py
  • tests/model_registry/model_catalog/catalog_config/test_catalog_source_merge.py
  • tests/model_registry/model_catalog/catalog_config/test_custom_model_catalog.py
  • tests/model_registry/model_catalog/catalog_config/test_default_model_catalog.py
  • tests/model_registry/model_catalog/catalog_config/utils.py
  • tests/model_registry/model_catalog/conftest.py
  • tests/model_registry/model_catalog/huggingface/test_huggingface_source_error_validation.py
  • tests/model_registry/model_catalog/huggingface/utils.py
  • tests/model_registry/model_catalog/metadata/test_custom_properties.py
  • tests/model_registry/model_catalog/metadata/test_filter_options_endpoint.py
  • tests/model_registry/model_catalog/metadata/test_sources_endpoint.py
  • tests/model_registry/model_catalog/metadata/utils.py
  • tests/model_registry/model_catalog/search/utils.py
  • tests/model_registry/model_catalog/sorting/utils.py
  • tests/model_registry/model_catalog/upgrade/test_model_catalog_upgrade.py
  • tests/model_registry/model_catalog/utils.py
  • tests/model_registry/utils.py

Comment thread tests/model_registry/mcp_servers/conftest.py
Comment thread tests/model_registry/mcp_servers/conftest.py
Comment thread tests/model_registry/mcp_servers/test_invalid_yaml.py Outdated
Comment thread tests/model_registry/mcp_servers/conftest.py Outdated
Comment thread tests/model_registry/mcp_servers/conftest.py
Comment thread tests/model_registry/mcp_servers/conftest.py
Comment thread tests/model_registry/mcp_servers/conftest.py
Comment thread tests/model_registry/mcp_servers/test_data_integrity.py
Comment thread tests/model_registry/mcp_servers/test_data_integrity.py
Comment thread tests/model_registry/mcp_servers/test_data_integrity.py Outdated
Comment thread tests/model_registry/mcp_servers/test_invalid_yaml.py
Copy link
Copy Markdown
Contributor

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

🧹 Nitpick comments (1)
tests/model_registry/mcp_servers/conftest.py (1)

87-90: Consider extracting the repeated YAML loading pattern.

The same pattern for loading and initializing mcp_catalogs appears in all three fixtures. This could be extracted to a helper function to reduce duplication.

♻️ Optional helper extraction
def _load_mcp_catalogs_data(catalog_config_map: ConfigMap) -> dict:
    """Load existing mcp_catalogs data from ConfigMap, initializing if absent."""
    current_data = yaml.safe_load(catalog_config_map.instance.data.get("sources.yaml", "{}") or "{}")
    if "mcp_catalogs" not in current_data:
        current_data["mcp_catalogs"] = []
    return current_data

Then replace the repeated blocks with:

current_data = _load_mcp_catalogs_data(catalog_config_map)
current_data["mcp_catalogs"].append(MCP_CATALOG_SOURCE)  # or extend, as needed

Also applies to: 128-131, 171-174

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/model_registry/mcp_servers/conftest.py` around lines 87 - 90, Extract
the repeated YAML-loading/initialization logic into a helper function (e.g.,
_load_mcp_catalogs_data) that accepts catalog_config_map (the ConfigMap-like
fixture) and returns the parsed dict with "mcp_catalogs" initialized if missing;
then replace the duplicated blocks in the three fixtures by calling that helper
and appending/ extending MCP_CATALOG_SOURCE to the returned dict before writing
back to catalog_config_map.instance.data["sources.yaml"]. Reference: the
repeated use of
yaml.safe_load(...catalog_config_map.instance.data.get("sources.yaml", "{}") or
"{}"), the MCP_CATALOG_SOURCE symbol, and the fixtures that manipulate
catalog_config_map.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/model_registry/mcp_servers/conftest.py`:
- Around line 87-90: Extract the repeated YAML-loading/initialization logic into
a helper function (e.g., _load_mcp_catalogs_data) that accepts
catalog_config_map (the ConfigMap-like fixture) and returns the parsed dict with
"mcp_catalogs" initialized if missing; then replace the duplicated blocks in the
three fixtures by calling that helper and appending/ extending
MCP_CATALOG_SOURCE to the returned dict before writing back to
catalog_config_map.instance.data["sources.yaml"]. Reference: the repeated use of
yaml.safe_load(...catalog_config_map.instance.data.get("sources.yaml", "{}") or
"{}"), the MCP_CATALOG_SOURCE symbol, and the fixtures that manipulate
catalog_config_map.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 49303754-dcd3-4fe1-9120-b16896115d20

📥 Commits

Reviewing files that changed from the base of the PR and between 7ec0033 and ff37418.

📒 Files selected for processing (4)
  • tests/model_registry/mcp_servers/conftest.py
  • tests/model_registry/mcp_servers/test_data_integrity.py
  • tests/model_registry/mcp_servers/test_invalid_yaml.py
  • tests/model_registry/mcp_servers/test_multi_source.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/model_registry/mcp_servers/test_multi_source.py

Copy link
Copy Markdown
Contributor

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

🧹 Nitpick comments (1)
tests/model_registry/mcp_servers/constants.py (1)

75-83: Avoid mutable module “constants” to prevent cross-test state bleed.

dict/set constants can be mutated by tests and cause order-dependent failures. Prefer immutable containers (MappingProxyType, frozenset, tuples).

Suggested hardening diff
+from types import MappingProxyType
+from typing import Final
@@
-MCP_CATALOG_SOURCE: dict = {
+MCP_CATALOG_SOURCE: Final = MappingProxyType({
     "name": MCP_CATALOG_SOURCE_NAME,
     "id": MCP_CATALOG_SOURCE_ID,
     "type": "yaml",
     "enabled": True,
-    "properties": {"yamlCatalogPath": MCP_SERVERS_YAML_CATALOG_PATH},
-    "labels": [MCP_CATALOG_SOURCE_NAME],
-}
+    "properties": MappingProxyType({"yamlCatalogPath": MCP_SERVERS_YAML_CATALOG_PATH}),
+    "labels": (MCP_CATALOG_SOURCE_NAME,),
+})
@@
-EXPECTED_MCP_SERVER_NAMES: set[str] = {"weather-api", "file-manager", "calculator"}
+EXPECTED_MCP_SERVER_NAMES: Final[frozenset[str]] = frozenset({"weather-api", "file-manager", "calculator"})

Also applies to: 106-113, 115-133, 157-174

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/model_registry/mcp_servers/constants.py` around lines 75 - 83,
MCP_CATALOG_SOURCE (and the other module-level dict/set constants in this file)
are mutable and can be changed by tests; make them immutable by importing
MappingProxyType from types and wrapping top-level dicts with MappingProxyType,
convert nested mutable sequences/sets (e.g., "labels") to tuples or frozenset,
and ensure "properties" maps are also wrapped with MappingProxyType; keep the
original constant names (MCP_CATALOG_SOURCE, MCP_CATALOG_SOURCE_NAME,
MCP_CATALOG_SOURCE_ID, MCP_SERVERS_YAML_CATALOG_PATH) but replace their literal
dict/list/set values with immutable equivalents so tests cannot mutate module
state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/model_registry/mcp_servers/constants.py`:
- Around line 75-83: MCP_CATALOG_SOURCE (and the other module-level dict/set
constants in this file) are mutable and can be changed by tests; make them
immutable by importing MappingProxyType from types and wrapping top-level dicts
with MappingProxyType, convert nested mutable sequences/sets (e.g., "labels") to
tuples or frozenset, and ensure "properties" maps are also wrapped with
MappingProxyType; keep the original constant names (MCP_CATALOG_SOURCE,
MCP_CATALOG_SOURCE_NAME, MCP_CATALOG_SOURCE_ID, MCP_SERVERS_YAML_CATALOG_PATH)
but replace their literal dict/list/set values with immutable equivalents so
tests cannot mutate module state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3b07088c-942c-4957-ab9e-3ab655f30ec3

📥 Commits

Reviewing files that changed from the base of the PR and between ff37418 and 2d19553.

📒 Files selected for processing (2)
  • tests/model_registry/mcp_servers/constants.py
  • tests/model_registry/mcp_servers/test_data_integrity.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/model_registry/mcp_servers/test_data_integrity.py

@dbasunag dbasunag merged commit 6204737 into opendatahub-io:main Mar 5, 2026
10 checks passed
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 5, 2026

Status of building tag latest: success.
Status of pushing tag latest to image registry: success.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants