Skip to content

Commit 73bf1a9

Browse files
fegedbasunag
andauthored
test: mcp-servers add named query filtering (#1216)
Implement named queries to filter MCP servers by predefined custom Co-authored-by: Debarati Basu-Nag <dbasunag@redhat.com>
1 parent 751f78b commit 73bf1a9

File tree

8 files changed

+213
-106
lines changed

8 files changed

+213
-106
lines changed

tests/model_registry/conftest.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
MCP_CATALOG_API_PATH,
3737
MCP_CATALOG_SOURCE,
3838
MCP_SERVERS_YAML,
39+
NAMED_QUERIES,
3940
)
4041
from tests.model_registry.utils import (
4142
generate_namespace_name,
@@ -492,10 +493,11 @@ def mcp_servers_configmap_patch(
492493
model_registry_rest_headers: dict[str, str],
493494
) -> Generator[None]:
494495
"""
495-
Class-scoped fixture that patches the model-catalog-sources ConfigMap
496+
Class-scoped fixture that patches the model-catalog-sources ConfigMap.
496497
497498
Sets two keys in the ConfigMap data:
498-
- sources.yaml: catalog source definition pointing to the MCP servers YAML
499+
- sources.yaml: catalog source definition pointing to the MCP servers YAML,
500+
plus named queries for filtering by custom properties
499501
- mcp-servers.yaml: the actual MCP server definitions
500502
"""
501503
catalog_config_map = ConfigMap(
@@ -508,6 +510,7 @@ def mcp_servers_configmap_patch(
508510
if "mcp_catalogs" not in current_data:
509511
current_data["mcp_catalogs"] = []
510512
current_data["mcp_catalogs"].append(MCP_CATALOG_SOURCE)
513+
current_data["namedQueries"] = NAMED_QUERIES
511514

512515
patches = {
513516
"data": {

tests/model_registry/mcp_servers/config/__init__.py

Whitespace-only changes.
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
from collections.abc import Generator
2+
3+
import pytest
4+
import yaml
5+
from kubernetes.dynamic import DynamicClient
6+
from ocp_resources.config_map import ConfigMap
7+
from ocp_resources.resource import ResourceEditor
8+
from simple_logger.logger import get_logger
9+
10+
from tests.model_registry.constants import DEFAULT_CUSTOM_MODEL_CATALOG
11+
from tests.model_registry.mcp_servers.constants import (
12+
MCP_CATALOG_INVALID_SOURCE,
13+
MCP_CATALOG_SOURCE,
14+
MCP_CATALOG_SOURCE2,
15+
MCP_SERVERS_YAML,
16+
MCP_SERVERS_YAML2,
17+
)
18+
from tests.model_registry.utils import (
19+
wait_for_mcp_catalog_api,
20+
wait_for_model_catalog_pod_ready_after_deletion,
21+
)
22+
23+
LOGGER = get_logger(name=__name__)
24+
25+
26+
@pytest.fixture(scope="class")
27+
def mcp_multi_source_configmap_patch(
28+
admin_client: DynamicClient,
29+
model_registry_namespace: str,
30+
mcp_catalog_rest_urls: list[str],
31+
model_registry_rest_headers: dict[str, str],
32+
) -> Generator[None]:
33+
"""
34+
Class-scoped fixture that patches the model-catalog-sources ConfigMap
35+
with two MCP catalog sources pointing to two different YAML files.
36+
"""
37+
catalog_config_map = ConfigMap(
38+
name=DEFAULT_CUSTOM_MODEL_CATALOG,
39+
client=admin_client,
40+
namespace=model_registry_namespace,
41+
)
42+
43+
current_data = yaml.safe_load(catalog_config_map.instance.data.get("sources.yaml", "{}") or "{}")
44+
if "mcp_catalogs" not in current_data:
45+
current_data["mcp_catalogs"] = []
46+
current_data["mcp_catalogs"].extend([MCP_CATALOG_SOURCE, MCP_CATALOG_SOURCE2])
47+
48+
patches = {
49+
"data": {
50+
"sources.yaml": yaml.dump(current_data, default_flow_style=False),
51+
"mcp-servers.yaml": MCP_SERVERS_YAML,
52+
"mcp-servers-2.yaml": MCP_SERVERS_YAML2,
53+
}
54+
}
55+
56+
with ResourceEditor(patches={catalog_config_map: patches}):
57+
wait_for_model_catalog_pod_ready_after_deletion(
58+
client=admin_client, model_registry_namespace=model_registry_namespace
59+
)
60+
wait_for_mcp_catalog_api(url=mcp_catalog_rest_urls[0], headers=model_registry_rest_headers)
61+
yield
62+
63+
wait_for_model_catalog_pod_ready_after_deletion(
64+
client=admin_client, model_registry_namespace=model_registry_namespace
65+
)
66+
67+
68+
@pytest.fixture(scope="class")
69+
def mcp_invalid_yaml_configmap_patch(
70+
request: pytest.FixtureRequest,
71+
admin_client: DynamicClient,
72+
model_registry_namespace: str,
73+
mcp_catalog_rest_urls: list[str],
74+
model_registry_rest_headers: dict[str, str],
75+
) -> Generator[None]:
76+
"""
77+
Class-scoped fixture that patches the ConfigMap with a valid MCP source
78+
plus an invalid one (parameterized via request.param as the invalid YAML content).
79+
"""
80+
catalog_config_map = ConfigMap(
81+
name=DEFAULT_CUSTOM_MODEL_CATALOG,
82+
client=admin_client,
83+
namespace=model_registry_namespace,
84+
)
85+
86+
current_data = yaml.safe_load(catalog_config_map.instance.data.get("sources.yaml", "{}") or "{}")
87+
if "mcp_catalogs" not in current_data:
88+
current_data["mcp_catalogs"] = []
89+
current_data["mcp_catalogs"].extend([MCP_CATALOG_SOURCE, MCP_CATALOG_INVALID_SOURCE])
90+
91+
patches = {
92+
"data": {
93+
"sources.yaml": yaml.dump(current_data, default_flow_style=False),
94+
"mcp-servers.yaml": MCP_SERVERS_YAML,
95+
"mcp-servers-invalid.yaml": request.param,
96+
}
97+
}
98+
99+
with ResourceEditor(patches={catalog_config_map: patches}):
100+
wait_for_model_catalog_pod_ready_after_deletion(
101+
client=admin_client, model_registry_namespace=model_registry_namespace
102+
)
103+
wait_for_mcp_catalog_api(url=mcp_catalog_rest_urls[0], headers=model_registry_rest_headers)
104+
yield
105+
106+
wait_for_model_catalog_pod_ready_after_deletion(
107+
client=admin_client, model_registry_namespace=model_registry_namespace
108+
)

tests/model_registry/mcp_servers/test_invalid_yaml.py renamed to tests/model_registry/mcp_servers/config/test_invalid_yaml.py

File renamed without changes.

tests/model_registry/mcp_servers/test_multi_source.py renamed to tests/model_registry/mcp_servers/config/test_multi_source.py

File renamed without changes.
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from typing import Self
2+
3+
import pytest
4+
from simple_logger.logger import get_logger
5+
6+
from tests.model_registry.mcp_servers.constants import CALCULATOR_PROVIDER, CALCULATOR_SERVER_NAME
7+
from tests.model_registry.utils import execute_get_command
8+
9+
LOGGER = get_logger(name=__name__)
10+
11+
12+
@pytest.mark.usefixtures("mcp_servers_configmap_patch")
13+
class TestMCPServerNamedQueries:
14+
"""RHOAIENG-52375: Tests for MCP server named query functionality."""
15+
16+
@pytest.mark.parametrize(
17+
"named_query, expected_custom_properties",
18+
[
19+
pytest.param(
20+
"production_ready",
21+
{"verifiedSource": True},
22+
id="production_ready",
23+
),
24+
pytest.param(
25+
"security_focused",
26+
{"sast": True, "readOnlyTools": True},
27+
id="security_focused",
28+
),
29+
],
30+
)
31+
def test_named_query_execution(
32+
self: Self,
33+
mcp_catalog_rest_urls: list[str],
34+
model_registry_rest_headers: dict[str, str],
35+
named_query: str,
36+
expected_custom_properties: dict[str, bool],
37+
):
38+
"""TC-API-011: Test executing a named query filters servers by custom properties."""
39+
response = execute_get_command(
40+
url=f"{mcp_catalog_rest_urls[0]}mcp_servers",
41+
headers=model_registry_rest_headers,
42+
params={"namedQuery": named_query},
43+
)
44+
items = response["items"]
45+
assert len(items) == 1, f"Expected 1 server matching '{named_query}', got {len(items)}"
46+
assert items[0]["name"] == CALCULATOR_SERVER_NAME
47+
48+
custom_props = items[0]["customProperties"]
49+
for prop_name, expected_value in expected_custom_properties.items():
50+
assert custom_props.get(prop_name, {}).get("bool_value") is expected_value, (
51+
f"Expected {prop_name}={expected_value}, got {custom_props.get(prop_name)}"
52+
)
53+
54+
@pytest.mark.parametrize(
55+
"filter_query, expected_count, expected_names",
56+
[
57+
pytest.param(
58+
f"provider='{CALCULATOR_PROVIDER}'",
59+
1,
60+
{CALCULATOR_SERVER_NAME},
61+
id="matching_overlap",
62+
),
63+
pytest.param(
64+
"provider='Weather Community'",
65+
0,
66+
set(),
67+
id="no_overlap",
68+
),
69+
],
70+
)
71+
def test_named_query_combined_with_filter_query(
72+
self: Self,
73+
mcp_catalog_rest_urls: list[str],
74+
model_registry_rest_headers: dict[str, str],
75+
filter_query: str,
76+
expected_count: int,
77+
expected_names: set[str],
78+
):
79+
"""TC-API-013: Test combining namedQuery with filterQuery."""
80+
response = execute_get_command(
81+
url=f"{mcp_catalog_rest_urls[0]}mcp_servers",
82+
headers=model_registry_rest_headers,
83+
params={"namedQuery": "production_ready", "filterQuery": filter_query},
84+
)
85+
items = response["items"]
86+
assert len(items) == expected_count, (
87+
f"Expected {expected_count} server(s) for namedQuery + '{filter_query}', got {len(items)}"
88+
)
89+
assert {server["name"] for server in items} == expected_names
Lines changed: 1 addition & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,7 @@
1-
from collections.abc import Generator
2-
31
import pytest
4-
import yaml
5-
from kubernetes.dynamic import DynamicClient
6-
from ocp_resources.config_map import ConfigMap
7-
from ocp_resources.resource import ResourceEditor
82
from simple_logger.logger import get_logger
93

10-
from tests.model_registry.constants import DEFAULT_CUSTOM_MODEL_CATALOG
11-
from tests.model_registry.mcp_servers.constants import (
12-
MCP_CATALOG_INVALID_SOURCE,
13-
MCP_CATALOG_SOURCE,
14-
MCP_CATALOG_SOURCE2,
15-
MCP_SERVERS_YAML,
16-
MCP_SERVERS_YAML2,
17-
)
18-
from tests.model_registry.utils import (
19-
execute_get_command,
20-
wait_for_mcp_catalog_api,
21-
wait_for_model_catalog_pod_ready_after_deletion,
22-
)
4+
from tests.model_registry.utils import execute_get_command
235

246
LOGGER = get_logger(name=__name__)
257

@@ -34,88 +16,3 @@ def mcp_servers_response(
3416
url=f"{mcp_catalog_rest_urls[0]}mcp_servers",
3517
headers=model_registry_rest_headers,
3618
)
37-
38-
39-
@pytest.fixture(scope="class")
40-
def mcp_multi_source_configmap_patch(
41-
admin_client: DynamicClient,
42-
model_registry_namespace: str,
43-
mcp_catalog_rest_urls: list[str],
44-
model_registry_rest_headers: dict[str, str],
45-
) -> Generator[None]:
46-
"""
47-
Class-scoped fixture that patches the model-catalog-sources ConfigMap
48-
with two MCP catalog sources pointing to two different YAML files.
49-
"""
50-
catalog_config_map = ConfigMap(
51-
name=DEFAULT_CUSTOM_MODEL_CATALOG,
52-
client=admin_client,
53-
namespace=model_registry_namespace,
54-
)
55-
56-
current_data = yaml.safe_load(catalog_config_map.instance.data.get("sources.yaml", "{}") or "{}")
57-
if "mcp_catalogs" not in current_data:
58-
current_data["mcp_catalogs"] = []
59-
current_data["mcp_catalogs"].extend([MCP_CATALOG_SOURCE, MCP_CATALOG_SOURCE2])
60-
61-
patches = {
62-
"data": {
63-
"sources.yaml": yaml.dump(current_data, default_flow_style=False),
64-
"mcp-servers.yaml": MCP_SERVERS_YAML,
65-
"mcp-servers-2.yaml": MCP_SERVERS_YAML2,
66-
}
67-
}
68-
69-
with ResourceEditor(patches={catalog_config_map: patches}):
70-
wait_for_model_catalog_pod_ready_after_deletion(
71-
client=admin_client, model_registry_namespace=model_registry_namespace
72-
)
73-
wait_for_mcp_catalog_api(url=mcp_catalog_rest_urls[0], headers=model_registry_rest_headers)
74-
yield
75-
76-
wait_for_model_catalog_pod_ready_after_deletion(
77-
client=admin_client, model_registry_namespace=model_registry_namespace
78-
)
79-
80-
81-
@pytest.fixture(scope="class")
82-
def mcp_invalid_yaml_configmap_patch(
83-
request: pytest.FixtureRequest,
84-
admin_client: DynamicClient,
85-
model_registry_namespace: str,
86-
mcp_catalog_rest_urls: list[str],
87-
model_registry_rest_headers: dict[str, str],
88-
) -> Generator[None]:
89-
"""
90-
Class-scoped fixture that patches the ConfigMap with a valid MCP source
91-
plus an invalid one (parameterized via request.param as the invalid YAML content).
92-
"""
93-
catalog_config_map = ConfigMap(
94-
name=DEFAULT_CUSTOM_MODEL_CATALOG,
95-
client=admin_client,
96-
namespace=model_registry_namespace,
97-
)
98-
99-
current_data = yaml.safe_load(catalog_config_map.instance.data.get("sources.yaml", "{}") or "{}")
100-
if "mcp_catalogs" not in current_data:
101-
current_data["mcp_catalogs"] = []
102-
current_data["mcp_catalogs"].extend([MCP_CATALOG_SOURCE, MCP_CATALOG_INVALID_SOURCE])
103-
104-
patches = {
105-
"data": {
106-
"sources.yaml": yaml.dump(current_data, default_flow_style=False),
107-
"mcp-servers.yaml": MCP_SERVERS_YAML,
108-
"mcp-servers-invalid.yaml": request.param,
109-
}
110-
}
111-
112-
with ResourceEditor(patches={catalog_config_map: patches}):
113-
wait_for_model_catalog_pod_ready_after_deletion(
114-
client=admin_client, model_registry_namespace=model_registry_namespace
115-
)
116-
wait_for_mcp_catalog_api(url=mcp_catalog_rest_urls[0], headers=model_registry_rest_headers)
117-
yield
118-
119-
wait_for_model_catalog_pod_ready_after_deletion(
120-
client=admin_client, model_registry_namespace=model_registry_namespace
121-
)

tests/model_registry/mcp_servers/constants.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,16 @@
165165
"labels": [MCP_CATALOG_INVALID_SOURCE_NAME],
166166
}
167167

168+
NAMED_QUERIES: dict = {
169+
"production_ready": {
170+
"verifiedSource": {"operator": "=", "value": True},
171+
},
172+
"security_focused": {
173+
"sast": {"operator": "=", "value": True},
174+
"readOnlyTools": {"operator": "=", "value": True},
175+
},
176+
}
177+
168178
EXPECTED_MCP_SOURCE2_SERVER_NAMES: set[str] = {"code-reviewer"}
169179
EXPECTED_ALL_MCP_SERVER_NAMES: set[str] = EXPECTED_MCP_SERVER_NAMES | EXPECTED_MCP_SOURCE2_SERVER_NAMES
170180

0 commit comments

Comments
 (0)