feat(clp-mcp-server): Add search_by_kql MCP tool call.#1436
Conversation
WalkthroughAdds CLI Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant CLI
participant Config
participant Server
participant Connector
participant SessionMgr
User->>CLI: run CLI with --config-path
CLI->>Config: read_yaml_config_file(config_path)
Config-->>CLI: CLPConfig (validated)
CLI->>Server: create_mcp_server(clp_config)
Server->>Connector: ClpConnector(clp_config)
rect rgba(200,220,255,0.25)
Note over User,Server: search_by_kql flow
User->>Server: search_by_kql(kql_query)
Server->>SessionMgr: start_session()
Server->>Connector: submit_query(kql_query)
Server->>Connector: wait_query_completion()
alt success
Connector-->>Server: query complete
Server->>Connector: read_query_results()
Server->>Server: sort_by_timestamp() → format_query_results()
Server->>SessionMgr: get_page_data(paged results)
Server-->>User: paged formatted results
else error / timeout
Connector-->>Server: Error / TimeoutError
Server-->>User: {"Error": "<message>"}
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: ASSERTIVE Plan: Pro 📒 Files selected for processing (3)
🧰 Additional context used🧠 Learnings (1)📚 Learning: 2025-10-20T20:19:40.504ZApplied to files:
🧬 Code graph analysis (2)components/clp-mcp-server/tests/server/test_utils.py (1)
components/clp-mcp-server/clp_mcp_server/server/server.py (4)
🪛 Ruff (0.14.1)components/clp-mcp-server/tests/server/test_utils.py10-23: Mutable class attributes should be annotated with (RUF012) 24-28: Mutable class attributes should be annotated with (RUF012) 31-40: Mutable class attributes should be annotated with (RUF012) 41-44: Mutable class attributes should be annotated with (RUF012) 47-59: Mutable class attributes should be annotated with (RUF012) 60-62: Mutable class attributes should be annotated with (RUF012) 65-104: Mutable class attributes should be annotated with (RUF012) 106-127: Mutable class attributes should be annotated with (RUF012) ⏰ 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)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
search_kql_query in MCP Server
search_kql_query in MCP Serversearch_kql_query tool call in MCP Server
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
components/clp-mcp-server/clp_mcp_server/clp_connector.py (3)
97-116: Return type mismatch: read_job_status claims QueryJobStatus but returns intThis works for equality checks (IntEnum), but it breaks type expectations and makes callers do ad‑hoc casts. Return the enum.
- async def read_job_status(self, query_id: str) -> QueryJobStatus: + async def read_job_status(self, query_id: str) -> QueryJobStatus: @@ - status = result[0] if result else None + raw_status = result[0] if result else None @@ - if status is None: + if raw_status is None: err_msg = f"Query job with ID {query_id} not found." raise ValueError(err_msg) - return status + return QueryJobStatus(raw_status)
117-141: Minor: avoid double enum wrapping and improve unknown-state messageAfter the previous fix, status is already a QueryJobStatus. Adjust accordingly.
- if status in error_states: - err_msg = ( - f"Query job with ID {query_id} ended in status {QueryJobStatus(status).name}." - ) + if status in error_states: + err_msg = f"Query job with ID {query_id} ended in status {status.name}." raise RuntimeError(err_msg) - if status not in waiting_states: - err_msg = f"Query job with ID {query_id} has unknown status {status}." + if status not in waiting_states: + err_msg = f"Query job with ID {query_id} has unknown status value: {int(status)}." raise RuntimeError(err_msg)
8-9: Fix incorrect MongoDB async client import — use Motor instead of PyMongoThe import will fail at runtime. PyMongo is synchronous and does not expose
AsyncMongoClient. The async client must come from Motor:-from pymongo import AsyncMongoClient +from motor.motor_asyncio import AsyncIOMotorClientAnd update the instantiation at line 28:
- mongo_client = AsyncMongoClient(mongo_url) + mongo_client = AsyncIOMotorClient(mongo_url)Ensure
motoris listed in your project dependencies.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (5)
components/clp-mcp-server/clp_mcp_server/clp_connector.py(4 hunks)components/clp-mcp-server/clp_mcp_server/clp_mcp_server.py(3 hunks)components/clp-mcp-server/clp_mcp_server/server/server.py(3 hunks)components/clp-mcp-server/clp_mcp_server/server/utils.py(1 hunks)components/clp-mcp-server/tests/server/test-utils.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
components/clp-mcp-server/tests/server/test-utils.py (1)
components/clp-mcp-server/clp_mcp_server/server/utils.py (2)
filter_query_results(41-58)sort_query_results(33-38)
components/clp-mcp-server/clp_mcp_server/server/server.py (3)
components/clp-mcp-server/clp_mcp_server/clp_connector.py (4)
ClpConnector(22-165)submit_query(40-95)wait_query_completion(117-150)read_results(152-165)components/clp-mcp-server/clp_mcp_server/server/session_manager.py (4)
SessionManager(156-236)start(175-178)cache_query_result_and_get_first_page(79-96)cache_query_result_and_get_first_page(193-203)components/clp-mcp-server/clp_mcp_server/server/utils.py (2)
filter_query_results(41-58)sort_query_results(33-38)
components/clp-mcp-server/clp_mcp_server/clp_connector.py (1)
components/clp-mcp-server/clp_mcp_server/constants.py (1)
QueryJobStatus(18-27)
components/clp-mcp-server/clp_mcp_server/clp_mcp_server.py (3)
components/clp-py-utils/clp_py_utils/clp_config.py (1)
CLPConfig(576-769)components/clp-py-utils/clp_py_utils/core.py (1)
read_yaml_config_file(56-62)components/clp-mcp-server/clp_mcp_server/server/server.py (1)
create_mcp_server(14-103)
🪛 Ruff (0.14.1)
components/clp-mcp-server/tests/server/test-utils.py
1-1: File components/clp-mcp-server/tests/server/test-utils.py is part of an implicit namespace package. Add an __init__.py.
(INP001)
9-40: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
42-58: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
61-65: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
66-68: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
71-84: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
85-89: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
92-101: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
102-105: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
130-130: No newline at end of file
Add trailing newline
(W292)
⏰ 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). (4)
- GitHub Check: package-image
- GitHub Check: lint-check (ubuntu-24.04)
- GitHub Check: rust-checks (ubuntu-24.04)
- GitHub Check: rust-checks (ubuntu-22.04)
🔇 Additional comments (1)
components/clp-mcp-server/clp_mcp_server/server/utils.py (1)
9-31: Minor: tighten error message and docstring for convert_epoch_to_date_stringCurrent behaviour is good. Consider clarifying that only ints in milliseconds are accepted; floats/str will raise TypeError (as intended).
Do you want me to add a tiny docstring example and a quick unit test for negative-but-valid timestamps (pre‑1970)?
There was a problem hiding this comment.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (1)
components/clp-mcp-server/tests/server/__init__.py(1 hunks)
🧰 Additional context used
🪛 Ruff (0.14.1)
components/clp-mcp-server/tests/server/__init__.py
1-1: No newline at end of file
Add trailing newline
(W292)
⏰ 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). (4)
- GitHub Check: package-image
- GitHub Check: rust-checks (ubuntu-22.04)
- GitHub Check: rust-checks (ubuntu-24.04)
- GitHub Check: lint-check (ubuntu-24.04)
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
components/clp-mcp-server/clp_mcp_server/server/server.py (1)
14-21: Document the clp_config parameter in create_mcp_server docstring.Apply:
def create_mcp_server(clp_config: Any) -> FastMCP: @@ - :return: A configured `FastMCP` instance. + :param clp_config: Parsed CLP configuration object (database and results cache settings). + :return: A configured `FastMCP` instance.
♻️ Duplicate comments (6)
components/clp-mcp-server/tests/server/test-utils.py (4)
1-1: Confirm tests package is explicit to avoid INP001.If missing, add components/clp-mcp-server/tests/init.py (empty) or tests/server/init.py.
#!/bin/bash # Verify explicit package to silence INP001 fd -a 'tests/__init__.py' components/clp-mcp-server || true fd -a 'tests/server/__init__.py' components/clp-mcp-server || true
3-3: Annotate class-level fixtures with ClassVar and import it to silence RUF012.Apply:
-from clp_mcp_server.server.utils import filter_query_results, sort_query_results +from typing import ClassVar +from clp_mcp_server.server.utils import filter_query_results, sort_query_results @@ - RAW_LOG_ENTRIES = [ + RAW_LOG_ENTRIES: ClassVar[list[dict]] = [ @@ - EXPECTED_RESULTS = [ + EXPECTED_RESULTS: ClassVar[list[str]] = [ @@ - MISSING_TIMESTAMP_AND_MESSAGE_ENTRY = [ + MISSING_TIMESTAMP_AND_MESSAGE_ENTRY: ClassVar[list[dict]] = [ @@ - MISSING_TIMESTAMP_AND_MESSAGE_EXPECTED = [ + MISSING_TIMESTAMP_AND_MESSAGE_EXPECTED: ClassVar[list[str]] = [ @@ - INVALID_TYPE_ENTRIES = [ + INVALID_TYPE_ENTRIES: ClassVar[list[dict]] = [ @@ - INVALID_TYPE_EXPECTED = [ + INVALID_TYPE_EXPECTED: ClassVar[list[str]] = [ @@ - INVALID_VALUE_ENTRIES = [ + INVALID_VALUE_ENTRIES: ClassVar[list[dict]] = [ @@ - INVALID_VALUE_EXPECTED = [ + INVALID_VALUE_EXPECTED: ClassVar[list[str]] = [Also applies to: 9-105
107-113: Add a robustness test for mixed timestamp types to protect against TypeError in sorting.Append:
+ def test_sort_with_mixed_timestamp_types(self): + mixed = [ + {"timestamp": "1729267200000", "message": "str ts"}, + {"timestamp": 1729267200000.0, "message": "float ts"}, + {"timestamp": None, "message": "none ts"}, + {"timestamp": 1729267200123, "message": "int ts"}, + ] + # sort should not raise; entries with bad/non-int timestamps end up after valid ints + sorted_result = sort_query_results(mixed) + assert sorted_result[0]["timestamp"] == 1729267200123 + # filtering should convert invalid timestamps to "N/A" + filtered = filter_query_results(sorted_result) + assert filtered[0].startswith("timestamp: 2024-10-18T16:00:00.123Z")
130-130: Ensure file ends with a single trailing newline to silence W292.components/clp-mcp-server/clp_mcp_server/server/server.py (1)
75-102: Guard empty KQL input to prevent submitting blank/whitespace-only jobs.Apply:
@mcp.tool async def search_kql_query(kql_query: str, ctx: Context) -> dict[str, object]: @@ - try: + try: + if not kql_query or not kql_query.strip(): + return {"Error": "Empty KQL query."} query_id = await connector.submit_query(kql_query) await connector.wait_query_completion(query_id) results = await connector.read_results(query_id) except (ValueError, RuntimeError, TimeoutError) as e: return {"Error": str(e)}components/clp-mcp-server/clp_mcp_server/server/utils.py (1)
33-41: Harden sort key to avoid TypeError with mixed timestamp types and ensure bad/missing values sort last.Apply:
-def sort_query_results(query_results: list[dict]) -> list[dict]: +def sort_query_results(query_results: list[dict]) -> list[dict]: @@ - return sorted(query_results, key=lambda log_entry: log_entry.get("timestamp", 0), reverse=True) + def _ts_key(entry: dict) -> int: + ts = entry.get("timestamp") + try: + return int(ts) + except (TypeError, ValueError): + return -1 # sentinel: oldest + return sorted(query_results, key=_ts_key, reverse=True)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (3)
components/clp-mcp-server/clp_mcp_server/server/server.py(3 hunks)components/clp-mcp-server/clp_mcp_server/server/utils.py(1 hunks)components/clp-mcp-server/tests/server/test-utils.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
components/clp-mcp-server/tests/server/test-utils.py (1)
components/clp-mcp-server/clp_mcp_server/server/utils.py (2)
filter_query_results(43-61)sort_query_results(33-40)
components/clp-mcp-server/clp_mcp_server/server/server.py (3)
components/clp-mcp-server/clp_mcp_server/clp_connector.py (4)
ClpConnector(22-165)submit_query(40-95)wait_query_completion(117-150)read_results(152-165)components/clp-mcp-server/clp_mcp_server/server/session_manager.py (4)
SessionManager(156-236)start(175-178)cache_query_result_and_get_first_page(79-96)cache_query_result_and_get_first_page(193-203)components/clp-mcp-server/clp_mcp_server/server/utils.py (2)
filter_query_results(43-61)sort_query_results(33-40)
🪛 Ruff (0.14.1)
components/clp-mcp-server/tests/server/test-utils.py
1-1: Invalid module name: 'test-utils'
(N999)
9-40: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
42-58: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
61-65: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
66-68: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
71-84: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
85-89: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
92-101: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
102-105: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
⏰ 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). (3)
- GitHub Check: rust-checks (ubuntu-24.04)
- GitHub Check: lint-check (ubuntu-24.04)
- GitHub Check: rust-checks (ubuntu-22.04)
🔇 Additional comments (2)
components/clp-mcp-server/clp_mcp_server/server/utils.py (1)
9-31: convert_epoch_to_date_string: solid validation and UTC ISO-8601 millisecond output. LGTM.components/clp-mcp-server/clp_mcp_server/server/server.py (1)
78-82: Docstring is accurate—no action needed.The
NUM_ITEMS_PER_PAGEconstant is set to 10, which aligns precisely with the docstring's claim of "10 log messages" per page. The contract is tight and correctly documented.
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (2)
components/clp-mcp-server/tests/server/__init__.py (1)
1-1: Add a trailing newline (PEP 8 / Ruff W292).File appears to end without a newline; add a single newline at EOF.
-"""Tests for server module.""" +"""Tests for server module."""components/clp-mcp-server/tests/server/test_utils.py (1)
107-113: Add a robustness test for mixed-type timestamps in sort.Current tests only cover int timestamps; add a case mixing int, str, float, and missing to ensure no TypeError and deterministic ordering.
Apply:
@@ def test_sort_and_filter_query_results(self): """Validates the functionality of post-processing for the query response.""" sorted_result = sort_query_results(self.RAW_LOG_ENTRIES) filtered_result = filter_query_results(sorted_result) assert filtered_result == self.EXPECTED_RESULTS + + def test_sort_handles_mixed_timestamp_types(self): + """sort_query_results should not raise on mixed timestamp types.""" + mixed = [ + {"timestamp": 2, "message": "int-2\n"}, + {"timestamp": "3", "message": "str-3\n"}, + {"timestamp": 1.0, "message": "float-1\n"}, + {"message": "missing\n"}, + ] + # Should not raise: + sorted_mixed = sort_query_results(mixed) + # After sorting, valid ints should come first (latest-to-oldest), others last. + assert sorted_mixed[0]["message"] == "int-2\n" + # Sanity: length unchanged + assert len(sorted_mixed) == 4 + # Filtering should still handle invalid types as N/A without raising: + _ = filter_query_results(sorted_mixed)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (2)
components/clp-mcp-server/tests/server/__init__.py(1 hunks)components/clp-mcp-server/tests/server/test_utils.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
components/clp-mcp-server/tests/server/test_utils.py (1)
components/clp-mcp-server/clp_mcp_server/server/utils.py (2)
filter_query_results(43-61)sort_query_results(33-40)
🪛 Ruff (0.14.1)
components/clp-mcp-server/tests/server/test_utils.py
9-40: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
42-58: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
61-65: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
66-68: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
71-84: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
85-89: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
92-101: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
102-105: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
⏰ 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). (2)
- GitHub Check: build (ubuntu-24.04)
- GitHub Check: lint-check (ubuntu-24.04)
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
components/clp-mcp-server/tests/server/test_utils.py (2)
107-113: LGTM! Test correctly validates the full pipeline.The test properly exercises both sorting and filtering with homogeneous integer timestamps. However, as noted in previous reviews, consider adding a test case for mixed-type timestamps to ensure
sort_query_resultshandles them without raisingTypeError.
9-105: Add ClassVar annotations to class-level test data.These class attributes should be annotated with
typing.ClassVarto satisfy RUF012 and clarify they are class-level constants, not mutable instance attributes.Based on static analysis hints.
Apply this diff:
-from clp_mcp_server.server.utils import filter_query_results, sort_query_results +from typing import ClassVar +from clp_mcp_server.server.utils import filter_query_results, sort_query_results class TestUtils: """Test suite for utility functions.""" # Testing basic functionality. - RAW_LOG_ENTRIES = [ + RAW_LOG_ENTRIES: ClassVar[list[dict]] = [ { "_id": "test001", "timestamp": 0, @@ -39,7 +40,7 @@ }, ] - EXPECTED_RESULTS = [ + EXPECTED_RESULTS: ClassVar[list[str]] = [ ( 'timestamp: 2024-10-18T16:00:00.123Z, message: ' '{"ts":1729267200123,"pid":1234,"tid":5678,' @@ -59,11 +60,11 @@ ] # Test case: missing timestamp and message fields. - MISSING_TIMESTAMP_AND_MESSAGE_ENTRY = [ + MISSING_TIMESTAMP_AND_MESSAGE_ENTRY: ClassVar[list[dict]] = [ { "_id": "test001", } ] - MISSING_TIMESTAMP_AND_MESSAGE_EXPECTED = [ + MISSING_TIMESTAMP_AND_MESSAGE_EXPECTED: ClassVar[list[str]] = [ "timestamp: N/A, message: " ] # Test case: invalid timestamp types. - INVALID_TYPE_ENTRIES = [ + INVALID_TYPE_ENTRIES: ClassVar[list[dict]] = [ { "timestamp": None, "message": '{"message":"Log with None timestamp"}\n', @@ -83,7 +84,7 @@ "message": '{"message":"Log with float timestamp"}\n', }, ] - INVALID_TYPE_EXPECTED = [ + INVALID_TYPE_EXPECTED: ClassVar[list[str]] = [ 'timestamp: N/A, message: {"message":"Log with None timestamp"}\n', 'timestamp: N/A, message: {"message":"Log with str timestamp"}\n', 'timestamp: N/A, message: {"message":"Log with float timestamp"}\n', @@ -91,7 +92,7 @@ # Test case: invalid timestamp values. - INVALID_VALUE_ENTRIES = [ + INVALID_VALUE_ENTRIES: ClassVar[list[dict]] = [ { "timestamp": 9999999999999999, "message": '{"message":"Log with overflow timestamp"}\n', @@ -101,7 +102,7 @@ "message": '{"message":"Log with negative overflow timestamp"}\n', }, ] - INVALID_VALUE_EXPECTED = [ + INVALID_VALUE_EXPECTED: ClassVar[list[str]] = [ 'timestamp: N/A, message: {"message":"Log with overflow timestamp"}\n', 'timestamp: N/A, message: {"message":"Log with negative overflow timestamp"}\n', ]
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (1)
components/clp-mcp-server/tests/server/test_utils.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
components/clp-mcp-server/tests/server/test_utils.py (1)
components/clp-mcp-server/clp_mcp_server/server/utils.py (2)
filter_query_results(43-61)sort_query_results(33-40)
🪛 Ruff (0.14.1)
components/clp-mcp-server/tests/server/test_utils.py
9-40: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
42-58: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
61-65: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
66-68: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
71-84: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
85-89: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
92-101: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
102-105: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
🔇 Additional comments (1)
components/clp-mcp-server/tests/server/test_utils.py (1)
5-6: LGTM! Well-structured test suite.The test class is appropriately named and documented.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (3)
components/clp-mcp-server/clp_mcp_server/server/utils.py (2)
48-49: Consider coercing message to string for robustness.If the
messagefield is accidentally a non-string type (e.g., int or dict), it will be interpolated directly into the formatted string, potentially producing unexpected output. Coerce it tostrfor defensive programming.Apply this diff:
- message = obj.get("message", "") - filtered.append(f"timestamp: {timestamp_str}, message: {message}") + message = str(obj.get("message", "")) + filtered.append(f"timestamp: {timestamp_str}, message: {message}")Optionally, add a test case with a non-string message to verify behavior.
54-61: Guard against TypeError with mixed-type timestamps.If any entry has a non-integer
timestamp(str/float/None), Python's sort will raise TypeError when comparing mixed key types. Add a safe key function that returns valid int timestamps as-is and a sentinel value for invalid types, ensuring invalid timestamps sort last.Apply this diff:
def sort_query_results(query_results: list[dict]) -> list[dict]: """ :param query_results: A list of dictionary containing log entries with its metadata read from MongoDB. :return: A sorted list of dictionary containing log entries with its metadata, ordered by epoch from latest to oldest. """ - return sorted(query_results, key=lambda log_entry: log_entry.get("timestamp", 0), reverse=True) + def _sort_key(log_entry: dict) -> int: + ts = log_entry.get("timestamp") + return ts if isinstance(ts, int) else -1 # Non-ints sort last + + return sorted(query_results, key=_sort_key, reverse=True)This aligns with
filter_query_results, which treats non-int timestamps as invalid ("N/A").components/clp-mcp-server/tests/server/test_utils.py (1)
3-106: Annotate class fixtures with ClassVar and add blank line after docstring.All class-level fixture attributes trigger RUF012 and should be annotated with
ClassVarto indicate they are class constants, not mutable instance attributes. Additionally, insert one blank line after the class docstring per D204.Based on static analysis hints.
Apply this diff:
-from clp_mcp_server.server.utils import filter_query_results, sort_query_results +from typing import ClassVar + +from clp_mcp_server.server.utils import filter_query_results, sort_query_results class TestUtils: """Test suite for utility functions.""" - + # Test case: invalid timestamp types. - INVALID_TYPE_ENTRIES = [ + INVALID_TYPE_ENTRIES: ClassVar[list[dict]] = [ { "timestamp": None, "message": '{"message":"Log with None timestamp"}\n', }, { "timestamp": "1729267200000", # str instead of int "message": '{"message":"Log with str timestamp"}\n', }, { "timestamp": 1729267200000.0, # float instead of int "message": '{"message":"Log with float timestamp"}\n', }, ] - INVALID_TYPE_EXPECTED = [ + INVALID_TYPE_EXPECTED: ClassVar[list[str]] = [ 'timestamp: N/A, message: {"message":"Log with None timestamp"}\n', 'timestamp: N/A, message: {"message":"Log with str timestamp"}\n', 'timestamp: N/A, message: {"message":"Log with float timestamp"}\n', ] # Test case: invalid timestamp values. - INVALID_VALUE_ENTRIES = [ + INVALID_VALUE_ENTRIES: ClassVar[list[dict]] = [ { "timestamp": 9999999999999999, "message": '{"message":"Log with overflow timestamp"}\n', }, { "timestamp": -9999999999999999, "message": '{"message":"Log with negative overflow timestamp"}\n', }, ] - INVALID_VALUE_EXPECTED = [ + INVALID_VALUE_EXPECTED: ClassVar[list[str]] = [ 'timestamp: N/A, message: {"message":"Log with overflow timestamp"}\n', 'timestamp: N/A, message: {"message":"Log with negative overflow timestamp"}\n', ] # Test case: missing timestamp and message fields. - MISSING_TIMESTAMP_AND_MESSAGE_ENTRY = [ + MISSING_TIMESTAMP_AND_MESSAGE_ENTRY: ClassVar[list[dict]] = [ { "_id": "test001", } ] - MISSING_TIMESTAMP_AND_MESSAGE_EXPECTED = [ + MISSING_TIMESTAMP_AND_MESSAGE_EXPECTED: ClassVar[list[str]] = [ "timestamp: N/A, message: " ] # Testing basic functionality. - RAW_LOG_ENTRIES = [ + RAW_LOG_ENTRIES: ClassVar[list[dict]] = [ { "_id": "test001", "timestamp": 0, "message": '{"ts":0,"pid":null,"tid":null,"message":"Log at epoch zero"}\n', "orig_file_path": "/var/log/app.log", "archive_id": "abc123", "log_event_ix": 100, }, { "_id": "test002", "timestamp": 1729267200000, # Oct 18, 2024 16:00:00.000 (milliseconds) "message": ( '{"ts":1729267200000,"pid":1234,"tid":5678,' '"message":"Log with zero milliseconds"}\n' ), "orig_file_path": "/var/log/app.log", "archive_id": "abc124", "log_event_ix": 101, }, { "_id": "test003", "timestamp": 1729267200123, # Oct 18, 2024 16:00:00.123 (milliseconds) "message": ( '{"ts":1729267200123,"pid":1234,"tid":5678,' '"message":"Log with millisecond precision"}\n' ), "orig_file_path": "/var/log/app.log", "archive_id": "abc125", "log_event_ix": 102, }, ] - EXPECTED_RESULTS = [ + EXPECTED_RESULTS: ClassVar[list[str]] = [ ( 'timestamp: 2024-10-18T16:00:00.123Z, message: ' '{"ts":1729267200123,"pid":1234,"tid":5678,' '"message":"Log with millisecond precision"}\n' ), ( 'timestamp: 2024-10-18T16:00:00.000Z, message: ' '{"ts":1729267200000,"pid":1234,"tid":5678,' '"message":"Log with zero milliseconds"}\n' ), ( 'timestamp: 1970-01-01T00:00:00.000Z, message: ' '{"ts":0,"pid":null,"tid":null,' '"message":"Log at epoch zero"}\n' ), ]
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (2)
components/clp-mcp-server/clp_mcp_server/server/utils.py(1 hunks)components/clp-mcp-server/tests/server/test_utils.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
components/clp-mcp-server/tests/server/test_utils.py (1)
components/clp-mcp-server/clp_mcp_server/server/utils.py (2)
filter_query_results(33-51)sort_query_results(54-61)
🪛 Ruff (0.14.1)
components/clp-mcp-server/tests/server/test_utils.py
6-6: 1 blank line required after class docstring
Insert 1 blank line after class docstring
(D204)
10-23: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
24-28: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
31-40: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
41-44: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
47-51: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
52-54: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
57-88: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
90-106: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
⏰ 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). (4)
- GitHub Check: package-image
- GitHub Check: lint-check (ubuntu-24.04)
- GitHub Check: rust-checks (ubuntu-22.04)
- GitHub Check: rust-checks (ubuntu-24.04)
🔇 Additional comments (1)
components/clp-mcp-server/clp_mcp_server/server/utils.py (1)
9-30: LGTM!The timestamp conversion function correctly validates input types, handles conversion errors gracefully, and produces ISO 8601 formatted strings with millisecond precision.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (3)
components/clp-mcp-server/clp_mcp_server/server/utils.py (1)
48-49: Consider explicit string coercion for defensive coding.While Python's f-string formatting implicitly converts non-string types, explicitly coercing the message field ensures consistent behaviour if unexpected types (e.g., dict, list) appear in the data.
Apply this diff if you prefer explicit type safety:
message = obj.get("message", "") - filtered.append(f"timestamp: {timestamp_str}, message: {message}") + filtered.append(f"timestamp: {timestamp_str}, message: {str(message)}")components/clp-mcp-server/tests/server/test_utils.py (2)
3-3: Add ClassVar import to satisfy static analysis.The class-level test data fixtures should be annotated with
ClassVarto indicate they are class variables, not mutable instance attributes.Based on static analysis hints.
Apply this diff:
+from typing import ClassVar + from clp_mcp_server.server.utils import filter_query_results, sort_query_results
9-119: Annotate class-level fixtures with ClassVar.All class-level test data fixtures should be annotated with
ClassVarto indicate they are class variables, not mutable instance attributes. This resolves the RUF012 warnings from static analysis.Based on static analysis hints.
Apply this diff:
"""Test suite for utility functions.""" # Test case: invalid timestamp types. - INVALID_TYPE_ENTRIES = [ + INVALID_TYPE_ENTRIES: ClassVar[list[dict]] = [ { "timestamp": None, "message": '{"message":"Log with None timestamp"}\n', }, { "timestamp": "1729267200000", # str instead of int "message": '{"message":"Log with str timestamp"}\n', }, { "timestamp": 1729267200000.0, # float instead of int "message": '{"message":"Log with float timestamp"}\n', }, ] - INVALID_TYPE_EXPECTED = [ + INVALID_TYPE_EXPECTED: ClassVar[list[str]] = [ 'timestamp: N/A, message: {"message":"Log with None timestamp"}\n', 'timestamp: N/A, message: {"message":"Log with str timestamp"}\n', 'timestamp: N/A, message: {"message":"Log with float timestamp"}\n', ] # Test case: invalid timestamp values. - INVALID_VALUE_ENTRIES = [ + INVALID_VALUE_ENTRIES: ClassVar[list[dict]] = [ { "timestamp": 9999999999999999, "message": '{"message":"Log with overflow timestamp"}\n', }, { "timestamp": -9999999999999999, "message": '{"message":"Log with negative overflow timestamp"}\n', }, ] - INVALID_VALUE_EXPECTED = [ + INVALID_VALUE_EXPECTED: ClassVar[list[str]] = [ 'timestamp: N/A, message: {"message":"Log with overflow timestamp"}\n', 'timestamp: N/A, message: {"message":"Log with negative overflow timestamp"}\n', ] # Test case: missing timestamp and message fields. - MISSING_TIMESTAMP_AND_MESSAGE_ENTRY = [ + MISSING_TIMESTAMP_AND_MESSAGE_ENTRY: ClassVar[list[dict]] = [ { "_id": "test001", } ] - MISSING_TIMESTAMP_AND_MESSAGE_EXPECTED = [ + MISSING_TIMESTAMP_AND_MESSAGE_EXPECTED: ClassVar[list[str]] = [ "timestamp: N/A, message: " ] # Testing basic functionality. - RAW_LOG_ENTRIES = [ + RAW_LOG_ENTRIES: ClassVar[list[dict]] = [ { "_id": "test000", "timestamp": None, ... }, ] - EXPECTED_RESULTS = [ + EXPECTED_RESULTS: ClassVar[list[str]] = [ ( 'timestamp: 2024-10-18T16:00:00.123Z, message: ' ... ), ]
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (2)
components/clp-mcp-server/clp_mcp_server/server/utils.py(1 hunks)components/clp-mcp-server/tests/server/test_utils.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
components/clp-mcp-server/tests/server/test_utils.py (1)
components/clp-mcp-server/clp_mcp_server/server/utils.py (2)
filter_query_results(33-51)sort_query_results(54-66)
🪛 Ruff (0.14.1)
components/clp-mcp-server/tests/server/test_utils.py
10-23: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
24-28: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
31-40: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
41-44: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
47-51: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
52-54: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
57-96: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
98-119: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
⏰ 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). (5)
- GitHub Check: package-image
- GitHub Check: lint-check (macos-15)
- GitHub Check: rust-checks (ubuntu-22.04)
- GitHub Check: rust-checks (macos-15)
- GitHub Check: rust-checks (ubuntu-24.04)
🔇 Additional comments (2)
components/clp-mcp-server/clp_mcp_server/server/utils.py (2)
9-30: LGTM: Robust timestamp conversion implementation.The function correctly enforces type safety, handles millisecond precision, and provides comprehensive error handling for edge cases.
54-66: LGTM: Robust sorting implementation handles mixed types.The
_keyfunction correctly handles non-integer timestamp types by returning a sentinel value, preventing TypeError during sorting and ensuring invalid timestamps sort last. This aligns well withfilter_query_resultstreating non-int timestamps as "N/A".
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
components/clp-mcp-server/clp_mcp_server/clp_mcp_server.py (1)
42-85: Fix control flow: return early on validation failures.When host or port validation fails (lines 47, 61, 66),
exit_codeis set to1but execution continues to load configuration and start the server. This wastes resources and may cause confusing error messages. Return early when validation fails.Apply this diff:
exit_code = 0 # Validate host and port if len(host.strip()) == 0: logger.error("Host cannot be empty.") - exit_code = 1 + return 1 # Validate host format (IP address or resolvable hostname) try: ipaddress.ip_address(host) except ValueError: # If not an IP, try to resolve as hostname try: socket.gethostbyname(host) except OSError: logger.exception( "Host validation failed: '%s' is not a valid IP address and DNS resolution failed.", host, ) - exit_code = 1 + return 1 max_port = 65535 if port <= 0 or port > max_port: logger.error("Port must be between 1 and %d, got: %d.", max_port, port) - exit_code = 1 + return 1 try: clp_config = CLPConfig.model_validate(read_yaml_config_file(config_path)) except ValidationError: logger.exception("Configuration validation failed for: %s", config_path) - exit_code = 1 + return 1 except Exception: logger.exception("Failed to load configuration from: %s", config_path) - exit_code = 1 + return 1 try: mcp = create_mcp_server(clp_config) logger.info("Starting CLP MCP Server on %s:%d.", host, port) mcp.run(transport="streamable-http", host=host, port=port) except Exception: logger.exception("Failed to start MCP server.") - exit_code = 1 - - return exit_code + return 1 + + return 0
♻️ Duplicate comments (4)
components/clp-mcp-server/clp_mcp_server/server/server.py (3)
15-23: Rename parameter to match its type and update docstring.The parameter is named
config_pathbut its type isCLPConfig, not a path. This is misleading. Rename toclp_configand document it properly.Apply this diff:
-def create_mcp_server(config_path: CLPConfig) -> FastMCP: +def create_mcp_server(clp_config: CLPConfig) -> FastMCP: """ Creates and defines API tool calls for the CLP MCP server. - :param config_path: + :param clp_config: The CLP configuration object. :return: A configured `FastMCP` instance. :raise: Propagates `FastMCP.__init__`'s exceptions. :raise: Propagates `FastMCP.tool`'s exceptions. """ mcp = FastMCP(name=constants.SERVER_NAME) session_manager = SessionManager(session_ttl_seconds=constants.SESSION_TTL_SECONDS) - connector = ClpConnector(config_path) + connector = ClpConnector(clp_config)Based on learnings.
79-96: Remove magic number and fix duplicate :return: sections in docstring.The docstring has two issues:
- Lines 82-83 hardcode "10 log messages", but this is a configurable constant (
NUM_ITEMS_PER_PAGE) that the return value already includes asnum_items_per_page.- Lines 87 and 94 both use
:return:, creating duplicate return documentation.Apply this diff:
""" Searches the logs for the specified Kibana Query Language (KQL) query and returns the first - page of the matching result. The paginated results are ordered from latest to oldest, with - each page containing the paging metadata and 10 log messages along with their date string - timestamps. + page of the matching result. The paginated results are ordered from latest to oldest. :param kql_query: :param ctx: The `FastMCP` context containing the metadata of the underlying MCP session. - :return: A dictionary containing the following key-value pairs on success: + :return: On success, a dictionary containing the following key-value pairs: - "items": A list of log entries in the requested page. - "num_total_pages": Total number of pages available from the query as an integer. - "num_total_items": Total number of log entries available from the query as an integer. - "num_items_per_page": Number of log entries per page. - "has_next": Whether a page exists after the returned one. - "has_previous": Whether a page exists before the returned one. - :return: A dictionary with the following key-value pair on failures: + On failure, a dictionary with the following key-value pair: - "Error": An error message describing the failure. """Based on learnings.
99-104: Add empty query validation and broaden exception handling.Two issues:
- No validation for empty or whitespace-only
kql_query. Submitting an empty query may cause unexpected behavior.- Exception handling only catches
ValueError,RuntimeError, andTimeoutError. Database/driver exceptions (e.g.,aiomysql.Error,pymongo.errors.PyMongoError) may escape and break the stable{"Error": "..."}response contract.Apply this diff:
await session_manager.start() try: + if not kql_query or not kql_query.strip(): + return {"Error": "Empty KQL query."} query_id = await connector.submit_query(kql_query) await connector.wait_query_completion(query_id) results = await connector.read_results(query_id) except (ValueError, RuntimeError, TimeoutError) as e: return {"Error": str(e)} + except Exception as e: + return {"Error": str(e)} sorted_results = sort_by_timestamp(results) filtered_results = filter_query_results(sorted_results)components/clp-mcp-server/clp_mcp_server/clp_mcp_server.py (1)
68-75: Include config path in error messages for better debugging.The error messages on lines 71 and 74 don't include the configuration file path, making it harder to debug issues. Include the
config_pathin the log messages to provide better context.Apply this diff:
try: clp_config = CLPConfig.model_validate(read_yaml_config_file(config_path)) except ValidationError: - logger.exception("Configuration validation failed.") + logger.exception("Configuration validation failed for: %s", config_path) exit_code = 1 except Exception: - logger.exception("Failed to load configuration.") + logger.exception("Failed to load configuration from: %s", config_path) exit_code = 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (3)
components/clp-mcp-server/clp_mcp_server/clp_connector.py(4 hunks)components/clp-mcp-server/clp_mcp_server/clp_mcp_server.py(3 hunks)components/clp-mcp-server/clp_mcp_server/server/server.py(3 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-20T20:19:40.504Z
Learnt from: 20001020ycx
PR: y-scope/clp#1436
File: components/clp-mcp-server/clp_mcp_server/clp_mcp_server.py:22-27
Timestamp: 2025-10-20T20:19:40.504Z
Learning: In the clp-mcp-server project (components/clp-mcp-server/clp_mcp_server/clp_mcp_server.py), the --config Click option should use exists=True to validate the configuration file path at option processing time, even though this requires the default path to exist on all machines.
Applied to files:
components/clp-mcp-server/clp_mcp_server/clp_mcp_server.py
🧬 Code graph analysis (3)
components/clp-mcp-server/clp_mcp_server/clp_mcp_server.py (3)
components/clp-py-utils/clp_py_utils/clp_config.py (1)
CLPConfig(557-739)components/clp-py-utils/clp_py_utils/core.py (1)
read_yaml_config_file(56-62)components/clp-mcp-server/clp_mcp_server/server/server.py (1)
create_mcp_server(15-112)
components/clp-mcp-server/clp_mcp_server/server/server.py (4)
components/clp-py-utils/clp_py_utils/clp_config.py (1)
CLPConfig(557-739)components/clp-mcp-server/clp_mcp_server/clp_connector.py (4)
ClpConnector(22-176)submit_query(42-98)wait_query_completion(120-154)read_results(156-176)components/clp-mcp-server/clp_mcp_server/server/session_manager.py (4)
SessionManager(157-237)start(176-179)cache_query_result_and_get_first_page(79-96)cache_query_result_and_get_first_page(194-204)components/clp-mcp-server/clp_mcp_server/server/utils.py (2)
filter_query_results(27-47)sort_by_timestamp(50-62)
components/clp-mcp-server/clp_mcp_server/clp_connector.py (1)
components/clp-py-utils/clp_py_utils/clp_config.py (1)
CLPConfig(557-739)
⏰ 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: package-image
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
components/clp-mcp-server/clp_mcp_server/clp_connector.py (2)
110-119: Blocker: read_job_status returns raw status (int/str), breaking enum comparisons.wait_query_completion expects a QueryJobStatus but receives a primitive; leads to “unknown status” and incorrect error paths.
Apply:
- async with aiomysql.connect(**self._db_conf) as conn, conn.cursor() as cur: + async with aiomysql.connect(**self._db_conf) as conn, conn.cursor() as cur: await cur.execute("SELECT status FROM query_jobs WHERE id = %s;", (query_id,)) result = await cur.fetchone() - status = result[0] if result else None + raw_status = result[0] if result else None + + status = QueryJobStatus(raw_status) if raw_status is not None else None
157-163: Complete return type annotation for read_results.Use concrete dict typing for clarity.
Apply:
- async def read_results(self, query_id: str) -> list[dict]: + async def read_results(self, query_id: str) -> list[dict[str, Any]]:
♻️ Duplicate comments (5)
components/clp-mcp-server/clp_mcp_server/clp_connector.py (1)
3-5: Type the constructor with CLPConfig and document the parameter.Improves type safety and clarity.
Apply:
-from typing import Any +from typing import Any +from clp_py_utils.clp_config import CLPConfig @@ - def __init__(self, clp_config: Any) -> None: - """Initializes the ClpConnector with MongoDB and MariaDB configurations.""" + def __init__(self, clp_config: CLPConfig) -> None: + """ + Initializes the ClpConnector with MongoDB and MariaDB configurations. + + :param clp_config: CLP configuration containing database, results cache and webui settings. + """Also applies to: 26-31
components/clp-mcp-server/tests/server/test_utils.py (1)
121-131: Add dedicated test for mixed-type sorting.Cover sort_by_timestamp with int/str/float/None timestamps to ensure no TypeError and stable ordering.
Example:
@@ def test_invalid_timestamp_value(self): """Validates the handling of invalid timestamp values.""" result = format_query_results(self.INVALID_VALUE_ENTRIES) assert result == self.INVALID_VALUE_EXPECTED + + def test_sort_by_timestamp_mixed_types(self): + """sort_by_timestamp handles heterogeneous timestamp types.""" + mixed = [ + {"timestamp": 1000, "message": "int 1000"}, + {"timestamp": "2000", "message": "str 2000"}, + {"timestamp": 500.5, "message": "float 500.5"}, + {"timestamp": None, "message": "none"}, + {"timestamp": 2000, "message": "int 2000"}, + ] + result = sort_by_timestamp(mixed) + assert result[0]["timestamp"] == 2000 + assert result[1]["timestamp"] == 1000 + assert not isinstance(result[-1]["timestamp"], int)components/clp-mcp-server/clp_mcp_server/server/server.py (1)
77-94: Harden tool boundary: empty KQL guard and catch‑all to preserve {"Error": "..."} contract.Prevents submitting blank jobs and leaks of driver exceptions.
Apply:
@mcp.tool async def search_by_kql(kql_query: str, ctx: Context) -> dict[str, Any]: @@ - await session_manager.start() + await session_manager.start() @@ - try: + try: + if not kql_query or not kql_query.strip(): + return {"Error": "Empty KQL query."} query_id = await connector.submit_query(kql_query) await connector.wait_query_completion(query_id) results = await connector.read_results(query_id) except (ValueError, RuntimeError, TimeoutError) as e: return {"Error": str(e)} + except Exception as e: + return {"Error": str(e)}Also applies to: 95-109
components/clp-mcp-server/clp_mcp_server/server/utils.py (2)
12-17: Docstring style: use “:raises” with the exception type.Keep Sphinx conventions consistent.
Apply:
- :raise: ValueError if `epoch_ts` cannot be converted to a valid date string. + :raises ValueError: If `epoch_ts` cannot be converted to a valid date string.
44-46: Coerce message to string to avoid non‑str leakage.Prevents accidental types (dict/int) from leaking into output.
Apply:
- message = obj.get("message", "") - filtered.append(f"timestamp: {timestamp_str}, message: {message}") + message = obj.get("message", "") + filtered.append(f"timestamp: {timestamp_str}, message: {str(message)}")
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (4)
components/clp-mcp-server/clp_mcp_server/clp_connector.py(2 hunks)components/clp-mcp-server/clp_mcp_server/server/server.py(3 hunks)components/clp-mcp-server/clp_mcp_server/server/utils.py(1 hunks)components/clp-mcp-server/tests/server/test_utils.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
components/clp-mcp-server/clp_mcp_server/server/server.py (4)
components/clp-py-utils/clp_py_utils/clp_config.py (1)
CLPConfig(557-739)components/clp-mcp-server/clp_mcp_server/clp_connector.py (4)
ClpConnector(23-177)submit_query(43-99)wait_query_completion(121-155)read_results(157-177)components/clp-mcp-server/clp_mcp_server/server/session_manager.py (4)
SessionManager(157-237)start(176-179)cache_query_result_and_get_first_page(79-96)cache_query_result_and_get_first_page(194-204)components/clp-mcp-server/clp_mcp_server/server/utils.py (2)
format_query_results(27-47)sort_by_timestamp(50-62)
components/clp-mcp-server/tests/server/test_utils.py (1)
components/clp-mcp-server/clp_mcp_server/server/utils.py (2)
format_query_results(27-47)sort_by_timestamp(50-62)
🪛 Ruff (0.14.1)
components/clp-mcp-server/tests/server/test_utils.py
10-23: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
24-28: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
31-40: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
41-44: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
47-51: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
52-54: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
57-96: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
98-119: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
⏰ 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). (3)
- GitHub Check: package-image
- GitHub Check: lint-check (ubuntu-24.04)
- GitHub Check: lint-check (macos-15)
| @@ -0,0 +1,149 @@ | |||
| """Unit tests for CLP MCP server's utility functions.""" | |||
|
|
|||
| from clp_mcp_server.server.utils import format_query_results, sort_by_timestamp | |||
There was a problem hiding this comment.
Fix RUF012: annotate class fixtures with ClassVar.
Mutable class attributes must be marked ClassVar to avoid instance attribute defaults and silence RUF012.
Apply:
-from clp_mcp_server.server.utils import format_query_results, sort_by_timestamp
+from typing import ClassVar
+from clp_mcp_server.server.utils import format_query_results, sort_by_timestamp
@@
- INVALID_TYPE_ENTRIES = [
+ INVALID_TYPE_ENTRIES: ClassVar[list[dict]] = [
@@
- INVALID_TYPE_EXPECTED = [
+ INVALID_TYPE_EXPECTED: ClassVar[list[str]] = [
@@
- INVALID_VALUE_ENTRIES = [
+ INVALID_VALUE_ENTRIES: ClassVar[list[dict]] = [
@@
- INVALID_VALUE_EXPECTED = [
+ INVALID_VALUE_EXPECTED: ClassVar[list[str]] = [
@@
- MISSING_TIMESTAMP_AND_MESSAGE_ENTRY = [
+ MISSING_TIMESTAMP_AND_MESSAGE_ENTRY: ClassVar[list[dict]] = [
@@
- MISSING_TIMESTAMP_AND_MESSAGE_EXPECTED = [
+ MISSING_TIMESTAMP_AND_MESSAGE_EXPECTED: ClassVar[list[str]] = [
@@
- RAW_LOG_ENTRIES = [
+ RAW_LOG_ENTRIES: ClassVar[list[dict]] = [
@@
- EXPECTED_RESULTS = [
+ EXPECTED_RESULTS: ClassVar[list[str]] = [As per static analysis hints.
Also applies to: 9-28, 31-44, 47-54, 57-96, 98-119
🤖 Prompt for AI Agents
components/clp-mcp-server/tests/server/test_utils.py lines 3 and ranges 9-28,
31-44, 47-54, 57-96, 98-119: the review flags mutable class-level fixtures that
must be annotated with typing.ClassVar to avoid being treated as instance
defaults (RUF012). Import ClassVar from typing at the top of the file and
annotate each class fixture/mutable class attribute with a ClassVar[...] type
(e.g., my_fixture: ClassVar[FixtureType] = ...) instead of plain assignment so
static analysis recognizes them as class variables; apply this change to all
class-level mutable attributes in the listed line ranges.
LinZhihao-723
left a comment
There was a problem hiding this comment.
Mostly docstring comments.
In the meanwhile I will go through the tests.
Otherwise lgtm.
| logger.warning("Failed to convert epoch timestamp=%s to date string: %s.", epoch, e) | ||
|
|
||
| message = obj.get("message", "") | ||
| filtered.append(f"timestamp: {timestamp_str}, message: {message}") |
There was a problem hiding this comment.
Just curious: why don't we return structured log events directly, but with other fields stripped and timestamp formatted as date strings?
There was a problem hiding this comment.
date string is for clear understanding of the time semantic because it is more natural lanuage process friendly compared to epoch.
Stripping other fields are my personal choice as I dont think they are useful and would consume extra token use.
Co-authored-by: Lin Zhihao <59785146+LinZhihao-723@users.noreply.github.com>
LinZhihao-723
left a comment
There was a problem hiding this comment.
Small fixes to unit tests.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (8)
components/clp-mcp-server/clp_mcp_server/clp_connector.py (2)
26-27: Document theclp_configparameter in the constructor docstring.The
clp_configparameter is missing from the docstring, as previously noted in review comments.Apply this diff to add the parameter documentation:
def __init__(self, clp_config: Any) -> None: - """Initializes the ClpConnector with MongoDB and MariaDB configurations.""" + """ + Initializes the ClpConnector with MongoDB and MariaDB configurations. + + :param clp_config: The CLP configuration object containing database and cache settings. + """
53-57: Use standard Sphinx:raisesformat for exception documentation.The current
:raise:format is non-standard. Sphinx convention uses:raises ExceptionType: description.Apply this diff:
- :raise: ValueError if `end_ts` is smaller than `begin_ts`. - :raise: RuntimeError if it fails to retrieve the ID of the submitted query. - :raise: aiomysql.Error if there is an error connecting to or querying MariaDB. - :raise: pymongo.errors.PyMongoError if there is an error interacting with MongoDB. - :raise: Exception for any other unexpected errors. + :raises ValueError: If `end_ts` is smaller than `begin_ts`. + :raises RuntimeError: If it fails to retrieve the ID of the submitted query. + :raises aiomysql.Error: If there is an error connecting to or querying MariaDB. + :raises pymongo.errors.PyMongoError: If there is an error interacting with MongoDB. + :raises Exception: For any other unexpected errors.components/clp-mcp-server/clp_mcp_server/server/server.py (3)
77-94: Consolidate duplicate:return:sections in docstring.The docstring has two
:return:sections (lines 85 and 92), which is non-standard. Consolidate into a single section describing both success and failure cases.Apply this diff:
:param kql_query: :param ctx: The `FastMCP` context containing the metadata of the underlying MCP session. - :return: A dictionary containing the following key-value pairs on success: + :return: On success, a dictionary containing the following key-value pairs: - "items": A list of log entries in the requested page. - "num_total_pages": Total number of pages available from the query as an integer. - "num_total_items": Total number of log entries available from the query as an integer. - "num_items_per_page": Number of log entries per page. - "has_next": Whether a page exists after the returned one. - "has_previous": Whether a page exists before the returned one. - :return: A dictionary with the following key-value pair on failures: + On failure, a dictionary with the following key-value pair: - "Error": An error message describing the failure.
97-102: Add validation for empty KQL query.The function doesn't validate whether
kql_queryis empty before submitting it, which could result in unnecessary database operations.Apply this diff to add validation:
await session_manager.start() try: + if not kql_query or not kql_query.strip(): + return {"Error": "Empty KQL query."} query_id = await connector.submit_query(kql_query) await connector.wait_query_completion(query_id) results = await connector.read_results(query_id)
101-102: Broaden exception handling to catch database exceptions.The current exception tuple only catches
(ValueError, RuntimeError, TimeoutError), butconnector.submit_query(),wait_query_completion(), andread_results()can raiseaiomysql.Errorandpymongo.errors.PyMongoErrorper their docstrings. These exceptions will escape the handler and break the stable{"Error": "..."}response shape.Apply this diff to add a catch-all handler:
except (ValueError, RuntimeError, TimeoutError) as e: return {"Error": str(e)} + except Exception as e: + return {"Error": str(e)}components/clp-mcp-server/clp_mcp_server/server/utils.py (3)
39-53: Rename variable to reflect its purpose.The variable
filtered(line 39) is misleading since the function formats results rather than filtering them. A previous review comment suggestedformatted_log_events.Apply this diff:
- filtered = [] + formatted_log_events = [] for obj in query_results: epoch = obj.get("timestamp") timestamp_str = TIMESTAMP_NOT_AVAILABLE if isinstance(epoch, int): try: timestamp_str = convert_epoch_to_date_string(epoch) except (TypeError, ValueError) as e: logger.warning("Failed to convert epoch timestamp=%s to date string: %s.", epoch, e) message = obj.get("message", "") - filtered.append(f"timestamp: {timestamp_str}, message: {message}") + formatted_log_events.append(f"timestamp: {timestamp_str}, message: {message}") - return filtered + return formatted_log_events
56-68: Expand docstring to clarify sorting behaviour and timestamp handling.The docstring should explicitly document the in-place sorting, expected timestamp format, and handling of missing/invalid timestamps, as suggested in previous review comments.
Apply this diff:
""" + Sorts the query results in-place by timestamp in descending order (latest to oldest). + + NOTE: + - Timestamp is expected to be an integer representing the epoch timestamp in milliseconds, + stored under the `timestamp` key. + - If `timestamp` is missing or not an integer, it is treated as the oldest possible timestamp. + :param query_results: A list of dictionaries representing kv-pair log events. - :return: The same list, sorted in-place by epoch from latest to oldest. + :return: The input list sorted in-place. """
12-24: Use standard Sphinx:raisesformat in docstring.Line 16 uses
:raise:but the standard Sphinx format is:raises ValueError:.Apply this diff:
- :raise: ValueError if `epoch_ts` cannot be converted to a valid date string. + :raises ValueError: If `epoch_ts` cannot be converted to a valid date string.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (3)
components/clp-mcp-server/clp_mcp_server/clp_connector.py(2 hunks)components/clp-mcp-server/clp_mcp_server/server/server.py(3 hunks)components/clp-mcp-server/clp_mcp_server/server/utils.py(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-20T20:19:40.504Z
Learnt from: 20001020ycx
PR: y-scope/clp#1436
File: components/clp-mcp-server/clp_mcp_server/clp_mcp_server.py:22-27
Timestamp: 2025-10-20T20:19:40.504Z
Learning: In the clp-mcp-server project (components/clp-mcp-server/clp_mcp_server/clp_mcp_server.py), the --config Click option should use exists=True to validate the configuration file path at option processing time, even though this requires the default path to exist on all machines.
Applied to files:
components/clp-mcp-server/clp_mcp_server/server/server.py
🧬 Code graph analysis (1)
components/clp-mcp-server/clp_mcp_server/server/server.py (4)
components/clp-py-utils/clp_py_utils/clp_config.py (1)
CLPConfig(557-739)components/clp-mcp-server/clp_mcp_server/clp_connector.py (4)
ClpConnector(23-177)submit_query(43-99)wait_query_completion(121-155)read_results(157-177)components/clp-mcp-server/clp_mcp_server/server/session_manager.py (4)
SessionManager(157-237)start(176-179)cache_query_result_and_get_first_page(79-96)cache_query_result_and_get_first_page(194-204)components/clp-mcp-server/clp_mcp_server/server/utils.py (2)
format_query_results(27-53)sort_by_timestamp(56-68)
⏰ 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). (3)
- GitHub Check: package-image
- GitHub Check: lint-check (ubuntu-24.04)
- GitHub Check: lint-check (macos-15)
🔇 Additional comments (2)
components/clp-mcp-server/clp_mcp_server/server/server.py (2)
5-5: LGTM!The new imports correctly support the KQL search feature: CLPConfig for configuration, ClpConnector for query execution, and utilities for result processing.
Also applies to: 8-8, 12-12
28-28: LGTM!The connector is correctly instantiated with the provided configuration.
Co-authored-by: Lin Zhihao <59785146+LinZhihao-723@users.noreply.github.com>
Co-authored-by: Lin Zhihao <59785146+LinZhihao-723@users.noreply.github.com>
…lp into 10-18-substring-search-api
search_by_kql tool call in MCP Serversearch_by_kql MCP tool call.
search_by_kql MCP tool call.search_by_kql MCP tool call.
search_by_kql MCP tool call.search_by_kql MCP tool call.
Co-authored-by: Lin Zhihao <59785146+LinZhihao-723@users.noreply.github.com>
Description
This PR implements search API for MCP server:
search_by_kql.The documentation of
search_by_kqlis listed below:search_by_kql(string: kql_query)Description:
Runs a search query that returns the 10 latest log messages that satisfy this KQL query (ordered by timestamp), the Agent can further access up to 1000 latest log messages that satisfy this KQL query using pagination. Pagination allows the Agent to query 10 log messages (one page) at a time using a page index. For example, the MCP agent can query log messages 100-110 using page index 10. The agent uses get_nth_page() to query a page from the search results.
Inputs:
kql_query: The query LLM wants to search for.
Outputs:
Errors:
Checklist
breaking change.
Validation performed
uv run --group dev pytest tests/server/test-utils.py -v.uv run ruff check clp/components/clp-mcp-server && task lint:check-py.task docker-images:packagewith docker compose. Connected Claude Desktop agent to the running MCP server service in docker compose. Manually instructed LLM to perform a substring_search_query, the retruned result is consistent with manual grep, with the correct formatted date string. You can see the end-to-end result using this linkSummary by CodeRabbit
New Features
Enhancements
Bug Fixes
Tests