Skip to content

feat: Add comprehensive test suite and enhance core logging/client#6

Open
nkissick-del wants to merge 1 commit intojmagar:mainfrom
nkissick-del:feature/core-improvements-and-tests
Open

feat: Add comprehensive test suite and enhance core logging/client#6
nkissick-del wants to merge 1 commit intojmagar:mainfrom
nkissick-del:feature/core-improvements-and-tests

Conversation

@nkissick-del
Copy link
Copy Markdown

@nkissick-del nkissick-del commented Feb 4, 2026

Overview

This PR introduces a comprehensive testing framework to ensure tool stability and improves security in the core client.

Changes

Testing Framework

  • Integration Validation: Added a new test script (tests/integration/test_live_queries.py) that verifies all MCP tools against a live server to catch schema mismatches.
  • Schema Compliance: Added a static analysis tool (tests/integration/test_schema_compliance.py) to ensure GraphQL queries validation.

Core Client Security

  • Data Redaction: Updated the client to automatically redact sensitive keys (like passwords and tokens) from logs.
  • Query Sanitization: Added sanitization logic to prevent secret variables in query strings from leaking into debug logs.

Verification

  • Ran the new integration suite against a test instance; all checks passed.

Summary by cubic

Adds a comprehensive test suite and hardens core client logging to catch API mismatches early and prevent secrets from appearing in logs.

  • New Features
    • Live GraphQL integration tests that run all tool queries against a server and report status.
    • Schema compliance checker that parses tool queries and validates root fields against the live introspected schema.
    • Unit tests for comment stripping and kilobyte formatting utilities.
    • Safer client logging: sanitizes query strings, redacts sensitive variables (passwords, tokens, secrets), treats idempotent Docker start/stop errors as success, and caps log files at 10MB with automatic reset.

Written for commit 5f48d25. Summary will update on new commits.

Summary by CodeRabbit

  • Tests

    • Added comprehensive integration tests for GraphQL query execution and schema compliance verification.
    • Added unit tests for utility functions.
  • New Features

    • Enhanced logging system with formatted output helpers for headers, separators, and status messages.
  • Bug Fixes

    • Improved error handling for GraphQL responses.
    • Enhanced security by masking sensitive information in logs.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

The changes add comprehensive test infrastructure including pytest configuration setup, integration tests for GraphQL schema compliance and live queries, unit tests for utilities, and enhance the client logging with query sanitization and variable masking alongside new logging helper functions.

Changes

Cohort / File(s) Summary
Test Configuration
tests/conftest.py
Adds pytest_configure hook to prepend the project root to sys.path, enabling tests to import the package directly.
Integration Tests
tests/integration/test_live_queries.py, tests/integration/test_schema_compliance.py
Introduces two new integration test scripts: one executes GraphQL queries against UnraidMCP server with result classification and error handling; the other validates schema compliance by introspecting the live schema and verifying discovered queries against it using AST-based query extraction.
Unit Tests
tests/unit/test_utils.py
Adds unit tests for utility functions: _strip_comments (comment removal with string preservation) and format_kb (byte formatting with edge case handling).
Logging Infrastructure
unraid_mcp/config/logging.py
Introduces eight new logging helper functions (log_header, log_with_level_and_indent, log_separator, log_error, log_warning, log_success, log_info, log_status) with formatting and style normalization. Narrows exception handling in OverwriteFileHandler to OSError only.
Core Client
unraid_mcp/core/client.py
Adds sanitize_query() utility and implements defensive logging: sanitizes GraphQL queries before logging and recursively redacts sensitive keys (password, token, secret, key) from variables payload.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 Hop, hop! Integration tests now bloom,
Schema compliance checks sweep the room,
Secrets masked, queries cleaned so bright,
While logs now help and guide the light!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 68.42% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: adding comprehensive tests (unit, integration) and enhancing core logging and client with security improvements for sensitive data redaction.

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

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

4 issues found across 6 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="unraid_mcp/core/client.py">

<violation number="1" location="unraid_mcp/core/client.py:31">
P2: The query sanitization regex stops at commas (`[^,)]+`), so variable defaults containing commas are only partially masked. This can leave sensitive default values in logs despite the intent to sanitize.</violation>
</file>

<file name="tests/integration/test_schema_compliance.py">

<violation number="1" location="tests/integration/test_schema_compliance.py:202">
P2: extract_root_field returns after the first root field token, so queries with multiple root fields only validate the first field and ignore the rest. This can let invalid root fields pass the compliance check.</violation>

<violation number="2" location="tests/integration/test_schema_compliance.py:262">
P2: Dynamically composed query strings are dropped because any f-string with variables returns `None`, so those queries are never validated by the compliance checker.</violation>
</file>

<file name="unraid_mcp/config/logging.py">

<violation number="1" location="unraid_mcp/config/logging.py:116">
P1: `tracebacks_show_locals=True` prints all local variables in tracebacks, which can leak sensitive secrets into logs. Consider disabling this (or making it opt-in) for production logging.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

show_path=False,
rich_tracebacks=True,
tracebacks_show_locals=True
tracebacks_show_locals=True,
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Feb 4, 2026

Choose a reason for hiding this comment

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

P1: tracebacks_show_locals=True prints all local variables in tracebacks, which can leak sensitive secrets into logs. Consider disabling this (or making it opt-in) for production logging.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At unraid_mcp/config/logging.py, line 116:

<comment>`tracebacks_show_locals=True` prints all local variables in tracebacks, which can leak sensitive secrets into logs. Consider disabling this (or making it opt-in) for production logging.</comment>

<file context>
@@ -111,20 +113,16 @@ def setup_logger(name: str = "UnraidMCPServer") -> logging.Logger:
         show_path=False,
         rich_tracebacks=True,
-        tracebacks_show_locals=True
+        tracebacks_show_locals=True,
     )
     console_handler.setLevel(numeric_log_level)
</file context>
Suggested change
tracebacks_show_locals=True,
tracebacks_show_locals=False,
Fix with Cubic

# Remove variable definitions and replace with placeholders
# This is a basic sanitization; for production, consider a proper GraphQL parser
# Remove variable definitions like $var: Type
query = re.sub(r"\$[a-zA-Z_][a-zA-Z0-9_]*\s*:\s*[^,)]+", "$VARIABLE", query)
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Feb 4, 2026

Choose a reason for hiding this comment

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

P2: The query sanitization regex stops at commas ([^,)]+), so variable defaults containing commas are only partially masked. This can leave sensitive default values in logs despite the intent to sanitize.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At unraid_mcp/core/client.py, line 31:

<comment>The query sanitization regex stops at commas (`[^,)]+`), so variable defaults containing commas are only partially masked. This can leave sensitive default values in logs despite the intent to sanitize.</comment>

<file context>
@@ -15,7 +16,21 @@
+    # Remove variable definitions and replace with placeholders
+    # This is a basic sanitization; for production, consider a proper GraphQL parser
+    # Remove variable definitions like $var: Type
+    query = re.sub(r"\$[a-zA-Z_][a-zA-Z0-9_]*\s*:\s*[^,)]+", "$VARIABLE", query)
+    # Truncate to safe length
+    return query[:500]
</file context>
Fix with Cubic

continue

# Token found (not an alias)
return op_type, token
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Feb 4, 2026

Choose a reason for hiding this comment

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

P2: extract_root_field returns after the first root field token, so queries with multiple root fields only validate the first field and ignore the rest. This can let invalid root fields pass the compliance check.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/integration/test_schema_compliance.py, line 202:

<comment>extract_root_field returns after the first root field token, so queries with multiple root fields only validate the first field and ignore the rest. This can let invalid root fields pass the compliance check.</comment>

<file context>
@@ -0,0 +1,483 @@
+            continue
+
+        # Token found (not an alias)
+        return op_type, token
+
+    return op_type, None
</file context>
Fix with Cubic

for value in node.values:
part = self._extract_string(value)
if part is None:
return None # Non-string part found (e.g. variable), abort
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Feb 4, 2026

Choose a reason for hiding this comment

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

P2: Dynamically composed query strings are dropped because any f-string with variables returns None, so those queries are never validated by the compliance checker.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/integration/test_schema_compliance.py, line 262:

<comment>Dynamically composed query strings are dropped because any f-string with variables returns `None`, so those queries are never validated by the compliance checker.</comment>

<file context>
@@ -0,0 +1,483 @@
+            for value in node.values:
+                part = self._extract_string(value)
+                if part is None:
+                    return None  # Non-string part found (e.g. variable), abort
+                parts.append(part)
+            return "".join(parts)
</file context>
Fix with Cubic

Copy link
Copy Markdown

@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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
unraid_mcp/config/logging.py (2)

49-87: ⚠️ Potential issue | 🟡 Minor

Fix size check to work with lazy stream initialization.

When delay=True, self.stream is None on first emit() call, causing the size check to be skipped. The first write occurs without validation, allowing the log file to exceed max_bytes.

Change the condition to check self.baseFilename instead, which is always set by the parent class regardless of stream state:

Suggested fix
-        if self.stream and hasattr(self.stream, "name"):
+        if hasattr(self, "baseFilename"):

161-196: ⚠️ Potential issue | 🟠 Major

Multiple OverwriteFileHandler instances targeting the same log file creates a race condition.

When fastmcp_logger and root_logger write through separate handler instances, each maintains its own file stream. When one handler detects the file has reached the 10MB cap, it closes its stream and deletes the file via os.remove(). The other handler's stream reference becomes stale, risking log loss or corruption on subsequent writes. Share a single handler instance between both loggers instead.

🛠️ Suggested fix (share one handler)
-    file_handler = OverwriteFileHandler(LOG_FILE_PATH, max_bytes=10 * 1024 * 1024, encoding="utf-8")
-    file_handler.setLevel(numeric_log_level)
-    file_formatter = logging.Formatter(
-        "%(asctime)s - %(name)s - %(levelname)s - %(module)s - %(funcName)s - %(lineno)d - %(message)s"
-    )
-    file_handler.setFormatter(file_formatter)
-    fastmcp_logger.addHandler(file_handler)
+    shared_file_handler = OverwriteFileHandler(
+        LOG_FILE_PATH, max_bytes=10 * 1024 * 1024, encoding="utf-8"
+    )
+    shared_file_handler.setLevel(numeric_log_level)
+    file_formatter = logging.Formatter(
+        "%(asctime)s - %(name)s - %(levelname)s - %(module)s - %(funcName)s - %(lineno)d - %(message)s"
+    )
+    shared_file_handler.setFormatter(file_formatter)
+    fastmcp_logger.addHandler(shared_file_handler)
@@
-    root_file_handler = OverwriteFileHandler(
-        LOG_FILE_PATH, max_bytes=10 * 1024 * 1024, encoding="utf-8"
-    )
-    root_file_handler.setLevel(numeric_log_level)
-    root_file_handler.setFormatter(file_formatter)
-    root_logger.addHandler(root_file_handler)
+    root_logger.addHandler(shared_file_handler)
🤖 Fix all issues with AI agents
In `@tests/conftest.py`:
- Around line 5-9: Rename the unused parameter in the pytest_configure hook from
config to _config to satisfy Ruff ARG001; update the function signature def
pytest_configure(config): to def pytest_configure(_config): and ensure any
references (there are none) would be updated accordingly so the hook still
matches pytest's expected signature while signaling the parameter is
intentionally unused.

In `@tests/integration/test_live_queries.py`:
- Around line 11-13: Replace the sys.path.append usage that adds PROJECT_ROOT
with sys.path.insert(0, str(PROJECT_ROOT)) so the local working-tree package
(PROJECT_ROOT) takes precedence over any installed packages; update the code
around the PROJECT_ROOT variable and the import modification (where
sys.path.append(str(PROJECT_ROOT)) is used) to use sys.path.insert(0,
str(PROJECT_ROOT)).

In `@tests/integration/test_schema_compliance.py`:
- Around line 238-248: The current heuristic in _check_value (which calls
_extract_string and works with val/stripped) uses startswith("query "), etc.,
and misses forms like "query($var...)" or "query\nName"; change the check to use
a word-boundary regex so it matches operations regardless of trailing space
(e.g., use re.match(r'^(query|mutation|subscription)\b', stripped)) and ensure
you import re at the top of the test file; update the conditional that currently
checks stripped.startswith(...) and "{" in stripped to use the new regex match
plus the existing "{" check.
- Around line 30-32: The test currently uses sys.path.append(PROJECT_ROOT) which
puts the repo at the end of sys.path and can cause an installed unraid_mcp
package to shadow the working tree; update the import prep to insert the project
root at the front of sys.path (use sys.path.insert(0, str(PROJECT_ROOT))) so the
local PROJECT_ROOT takes precedence—follow the same pattern used in
tests/conftest.py and replace the sys.path.append call accordingly (identify the
code around PROJECT_ROOT and sys.path.append to change).

In `@tests/unit/test_utils.py`:
- Around line 31-43: The format_kb function currently calls int(k) without
guarding against non-numeric or infinite values; update the code around the
int(k) conversion in format_kb to wrap it in a try-except that catches
TypeError, ValueError, and OverflowError and on those exceptions returns str(k)
(preserving "inf" and arbitrary non-numeric strings), while keeping the existing
special-case handling for None to return "N/A"; ensure the conversion fallback
prevents the tests for float("inf") and "not_a_number" from raising.

In `@unraid_mcp/core/client.py`:
- Around line 22-33: The sanitize_query function currently only masks GraphQL
variable definitions; update sanitize_query to also mask string literals to
prevent inline secrets by first replacing block strings (triple-quoted GraphQL
block strings) and then normal quoted strings (handling escaped quotes) with a
placeholder like "$STRING", keeping the existing variable-definition masking and
truncation behavior; reference the sanitize_query function and ensure the
replacement order handles block strings before normal strings to avoid partial
matches.

Comment on lines +5 to +9
def pytest_configure(config):
"""Add project root to sys.path so tests can import the package."""
project_root = Path(__file__).parent.parent
if str(project_root) not in sys.path:
sys.path.insert(0, str(project_root))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cd /tmp && find . -name "conftest.py" -type f 2>/dev/null | head -20

Repository: jmagar/unraid-mcp

Length of output: 43


🏁 Script executed:

cat -n tests/conftest.py 2>/dev/null || echo "File not found in root"

Repository: jmagar/unraid-mcp

Length of output: 399


🏁 Script executed:

# Check if this is a pytest project and get more context
ls -la tests/ 2>/dev/null | head -20

Repository: jmagar/unraid-mcp

Length of output: 342


🏁 Script executed:

# Search for pytest_configure usage in the codebase
rg "pytest_configure" --type py

Repository: jmagar/unraid-mcp

Length of output: 108


🏁 Script executed:

# Look for ruff configuration files
find . -type f \( -name "pyproject.toml" -o -name "ruff.toml" -o -name ".ruff.toml" \) | head -10

Repository: jmagar/unraid-mcp

Length of output: 89


🏁 Script executed:

# Check for any existing noqa comments in the file
cat -n tests/conftest.py

Repository: jmagar/unraid-mcp

Length of output: 399


🏁 Script executed:

# Check if there are any ruff rules configured in pyproject.toml
fd "pyproject.toml" -x cat {} \; 2>/dev/null | head -100

Repository: jmagar/unraid-mcp

Length of output: 2296


🏁 Script executed:

cat -n ruff.toml

Repository: jmagar/unraid-mcp

Length of output: 2286


🏁 Script executed:

# Check if ARG rule is enabled in ruff config
cat ruff.toml && echo "---" && cat pyproject.toml | grep -A 20 "\[tool.ruff"

Repository: jmagar/unraid-mcp

Length of output: 2806


Rename unused config parameter to silence Ruff ARG001.

The pytest_configure hook requires the parameter by signature, but config is unused in the function body. Rename it to _config to follow Python convention for intentionally unused parameters.

Suggested fix
-def pytest_configure(config):
+def pytest_configure(_config):
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 5-5: Unused function argument: config

(ARG001)

🤖 Prompt for AI Agents
In `@tests/conftest.py` around lines 5 - 9, Rename the unused parameter in the
pytest_configure hook from config to _config to satisfy Ruff ARG001; update the
function signature def pytest_configure(config): to def
pytest_configure(_config): and ensure any references (there are none) would be
updated accordingly so the hook still matches pytest's expected signature while
signaling the parameter is intentionally unused.

Comment on lines +11 to +13
# Add project root to path
PROJECT_ROOT = Path(__file__).parent.parent.parent
sys.path.append(str(PROJECT_ROOT))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, find and read the target file
find . -name "test_live_queries.py" -type f

Repository: jmagar/unraid-mcp

Length of output: 101


🏁 Script executed:

# Also search for sys.path usage patterns in tests directory
rg "sys\.path" tests/ -A 2 -B 2

Repository: jmagar/unraid-mcp

Length of output: 1197


🏁 Script executed:

# Check if there's an installed unraid_mcp package referenced
rg "unraid_mcp" . --include="*.py" | head -20

Repository: jmagar/unraid-mcp

Length of output: 502


🏁 Script executed:

# Read the actual test_live_queries.py file to see line numbers and full context
head -20 tests/integration/test_live_queries.py

Repository: jmagar/unraid-mcp

Length of output: 534


🏁 Script executed:

# Check the conftest.py to understand the import setup
cat tests/conftest.py

Repository: jmagar/unraid-mcp

Length of output: 336


🏁 Script executed:

# Check if there's a setup.py or pyproject.toml to understand package structure
ls -la | grep -E "setup|pyproject"

Repository: jmagar/unraid-mcp

Length of output: 126


🏁 Script executed:

# Verify pyproject.toml to confirm unraid_mcp is a real package that could be installed
cat pyproject.toml | head -30

Repository: jmagar/unraid-mcp

Length of output: 957


🏁 Script executed:

# Check if there are any other sys.path manipulations in integration tests
rg "sys\.path" tests/integration/ -A 1 -B 1

Repository: jmagar/unraid-mcp

Length of output: 486


Use sys.path.insert(0, ...) to ensure the local package takes precedence on import.

sys.path.append() places the project root after site-packages, allowing an installed unraid_mcp package to be imported instead of the working tree version. This matters if the test is run directly as a script (bypassing pytest's conftest). Use insert(0, ...) instead.

🛠️ Suggested fix
-sys.path.append(str(PROJECT_ROOT))
+sys.path.insert(0, str(PROJECT_ROOT))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Add project root to path
PROJECT_ROOT = Path(__file__).parent.parent.parent
sys.path.append(str(PROJECT_ROOT))
# Add project root to path
PROJECT_ROOT = Path(__file__).parent.parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
🤖 Prompt for AI Agents
In `@tests/integration/test_live_queries.py` around lines 11 - 13, Replace the
sys.path.append usage that adds PROJECT_ROOT with sys.path.insert(0,
str(PROJECT_ROOT)) so the local working-tree package (PROJECT_ROOT) takes
precedence over any installed packages; update the code around the PROJECT_ROOT
variable and the import modification (where sys.path.append(str(PROJECT_ROOT))
is used) to use sys.path.insert(0, str(PROJECT_ROOT)).

Comment on lines +30 to +32
# Add project root to path to allow imports if needed, though we primarily parse text
PROJECT_ROOT = Path(__file__).parent.parent.parent
sys.path.append(str(PROJECT_ROOT))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n tests/integration/test_schema_compliance.py | head -40

Repository: jmagar/unraid-mcp

Length of output: 1507


🏁 Script executed:

rg "sys.path\.(append|insert)" --type py

Repository: jmagar/unraid-mcp

Length of output: 277


🏁 Script executed:

rg "sys.path.insert" -B 3 -A 3 tests/conftest.py

Repository: jmagar/unraid-mcp

Length of output: 268


Ensure the local package takes precedence on import.

sys.path.append places the repo after site-packages, so an installed unraid_mcp could be imported instead of the working tree. Use sys.path.insert(0, ...) to match the pattern already established in tests/conftest.py.

🛠️ Suggested fix
-sys.path.append(str(PROJECT_ROOT))
+sys.path.insert(0, str(PROJECT_ROOT))
🤖 Prompt for AI Agents
In `@tests/integration/test_schema_compliance.py` around lines 30 - 32, The test
currently uses sys.path.append(PROJECT_ROOT) which puts the repo at the end of
sys.path and can cause an installed unraid_mcp package to shadow the working
tree; update the import prep to insert the project root at the front of sys.path
(use sys.path.insert(0, str(PROJECT_ROOT))) so the local PROJECT_ROOT takes
precedence—follow the same pattern used in tests/conftest.py and replace the
sys.path.append call accordingly (identify the code around PROJECT_ROOT and
sys.path.append to change).

Comment on lines +238 to +248
def _check_value(self, node, lineno):
val = self._extract_string(node)
if val:
stripped = val.strip()
# Simple heuristic: must look like a GraphQL operation or shorthand
if (
stripped.startswith("query ")
or stripped.startswith("mutation ")
or stripped.startswith("subscription ")
or stripped.startswith("{")
) and "{" in stripped:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Query discovery misses operations without a trailing space.

startswith("query ") won’t match query($var: ...) or query\nName, so some queries may be skipped. Use a keyword match to capture those formats.

🛠️ Suggested fix
-            if (
-                stripped.startswith("query ")
-                or stripped.startswith("mutation ")
-                or stripped.startswith("subscription ")
-                or stripped.startswith("{")
-            ) and "{" in stripped:
+            if (
+                re.match(r"^(query|mutation|subscription)\b", stripped)
+                or stripped.startswith("{")
+            ) and "{" in stripped:
🛠️ Add import
-import ast
+import ast
+import re
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 238-238: Missing return type annotation for private function _check_value

Add return type annotation: None

(ANN202)


[warning] 244-247: Call startswith once with a tuple

Merge into a single startswith call

(PIE810)

🤖 Prompt for AI Agents
In `@tests/integration/test_schema_compliance.py` around lines 238 - 248, The
current heuristic in _check_value (which calls _extract_string and works with
val/stripped) uses startswith("query "), etc., and misses forms like
"query($var...)" or "query\nName"; change the check to use a word-boundary regex
so it matches operations regardless of trailing space (e.g., use
re.match(r'^(query|mutation|subscription)\b', stripped)) and ensure you import
re at the top of the test file; update the conditional that currently checks
stripped.startswith(...) and "{" in stripped to use the new regex match plus the
existing "{" check.

Comment on lines +31 to +43
def test_format_kb(self):
"""
Verify the logic of the real format_kb function directly.
"""
# Test overflow/special cases
self.assertEqual(format_kb(float("inf")), "inf")
self.assertEqual(format_kb("not_a_number"), "not_a_number")
self.assertEqual(format_kb(None), "N/A")

# Test formatting
self.assertEqual(format_kb(1024), "1.00 MB")
self.assertEqual(format_kb(1024 * 1024), "1.00 GB")
self.assertEqual(format_kb(512), "512 KB")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find and inspect the format_kb function
rg -n "def format_kb" -A 15

Repository: jmagar/unraid-mcp

Length of output: 1274


🏁 Script executed:

# Check the test file to confirm the assertions
cat -n tests/unit/test_utils.py | sed -n '31,43p'

Repository: jmagar/unraid-mcp

Length of output: 674


🏁 Script executed:

# Look for the format_kb import in the test file
rg -n "from.*format_kb|import.*format_kb" tests/unit/test_utils.py -B 2 -A 2

Repository: jmagar/unraid-mcp

Length of output: 250


format_kb needs error handling to make the edge-case tests pass.

The function uses int() without error handling on line 114 of unraid_mcp/tools/system.py. This will raise OverflowError for float("inf") and ValueError for non-numeric strings like "not_a_number", causing the test assertions on lines 36–37 to fail.

Wrap the int(k) call in a try-except block to catch TypeError, ValueError, and OverflowError, returning str(k) as a fallback:

Suggested fix
 def format_kb(k: Any) -> str:
     if k is None:
         return "N/A"
-    k = int(k) # Values are strings in SDL for PrefixedID containing types like capacity
+    try:
+        k = int(k)  # Values are strings in SDL for PrefixedID containing types like capacity
+    except (TypeError, ValueError, OverflowError):
+        return str(k)
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 36-36: Use a regular assert instead of unittest-style assertEqual

Replace assertEqual(...) with assert ...

(PT009)


[warning] 37-37: Use a regular assert instead of unittest-style assertEqual

Replace assertEqual(...) with assert ...

(PT009)


[warning] 38-38: Use a regular assert instead of unittest-style assertEqual

Replace assertEqual(...) with assert ...

(PT009)


[warning] 41-41: Use a regular assert instead of unittest-style assertEqual

Replace assertEqual(...) with assert ...

(PT009)


[warning] 42-42: Use a regular assert instead of unittest-style assertEqual

Replace assertEqual(...) with assert ...

(PT009)


[warning] 43-43: Use a regular assert instead of unittest-style assertEqual

Replace assertEqual(...) with assert ...

(PT009)

🤖 Prompt for AI Agents
In `@tests/unit/test_utils.py` around lines 31 - 43, The format_kb function
currently calls int(k) without guarding against non-numeric or infinite values;
update the code around the int(k) conversion in format_kb to wrap it in a
try-except that catches TypeError, ValueError, and OverflowError and on those
exceptions returns str(k) (preserving "inf" and arbitrary non-numeric strings),
while keeping the existing special-case handling for None to return "N/A";
ensure the conversion fallback prevents the tests for float("inf") and
"not_a_number" from raising.

Comment on lines +22 to +33
def sanitize_query(query: str) -> str:
"""Remove potential secrets from query string before logging.

Primary secret protection is performed by variables masking. This function
serves as defense-in-depth for type naming and definitions.
"""
# Remove variable definitions and replace with placeholders
# This is a basic sanitization; for production, consider a proper GraphQL parser
# Remove variable definitions like $var: Type
query = re.sub(r"\$[a-zA-Z_][a-zA-Z0-9_]*\s*:\s*[^,)]+", "$VARIABLE", query)
# Truncate to safe length
return query[:500]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and examine the file
find . -name "client.py" -path "*/unraid_mcp/core/*"

Repository: jmagar/unraid-mcp

Length of output: 88


🏁 Script executed:

# Read the specific file to see the current implementation
cat -n unraid_mcp/core/client.py | head -50

Repository: jmagar/unraid-mcp

Length of output: 2156


🏁 Script executed:

# Search for usage of sanitize_query to understand context
rg "sanitize_query" -B 2 -A 2

Repository: jmagar/unraid-mcp

Length of output: 593


🏁 Script executed:

# Look for tests of sanitize_query
find . -name "*test*.py" -type f

Repository: jmagar/unraid-mcp

Length of output: 194


🏁 Script executed:

# Search for any tests related to sanitize_query
rg "sanitize_query" --include="*test*.py"

Repository: jmagar/unraid-mcp

Length of output: 502


🏁 Script executed:

# Check if there are any other security measures in the codebase for logging
rg "logger.debug.*query|logger.info.*query" -B 2 -A 2

Repository: jmagar/unraid-mcp

Length of output: 2157


🏁 Script executed:

# Look at the full context of how sanitized queries are logged
rg "sanitized = sanitize_query" -B 5 -A 5

Repository: jmagar/unraid-mcp

Length of output: 790


🏁 Script executed:

# Test the suggested regex pattern to understand what it captures
python3 <<'EOF'
import re

# Test cases
test_queries = [
    'mutation { login(password: "secret123") }',
    'query { user(id: "123", token: "abc") }',
    'mutation { create(data: """multiline\nstring""") }',
    'query { search(term: "hello\\"world") }',  # escaped quote
]

pattern = r'""".*?"""|"(?:\\.|[^"\\])*"'

for query in test_queries:
    print(f"Original: {query}")
    masked = re.sub(pattern, '""', query, flags=re.DOTALL)
    print(f"Masked:   {masked}")
    print()
EOF

Repository: jmagar/unraid-mcp

Length of output: 439


🏁 Script executed:

# Check if there are existing tests for the sanitize_query function
cat -n ./tests/unit/test_utils.py | head -100

Repository: jmagar/unraid-mcp

Length of output: 1942


Add string literal masking to prevent inline secrets from reaching logs.

The current regex only removes GraphQL variable definitions ($var: Type). Inline argument values like password: "secret" remain exposed in logs. Add masking for string literals (including escaped quotes and block strings):

Suggested fix
     # Remove variable definitions like $var: Type
     query = re.sub(r"\$[a-zA-Z_][a-zA-Z0-9_]*\s*:\s*[^,)]+", "$VARIABLE", query)
+    # Replace string literals (including block strings) to avoid inline secret leakage
+    query = re.sub(r'""".*?"""|"(?:\\.|[^"\\])*"', '""', query, flags=re.DOTALL)
     # Truncate to safe length
     return query[:500]
🤖 Prompt for AI Agents
In `@unraid_mcp/core/client.py` around lines 22 - 33, The sanitize_query function
currently only masks GraphQL variable definitions; update sanitize_query to also
mask string literals to prevent inline secrets by first replacing block strings
(triple-quoted GraphQL block strings) and then normal quoted strings (handling
escaped quotes) with a placeholder like "$STRING", keeping the existing
variable-definition masking and truncation behavior; reference the
sanitize_query function and ensure the replacement order handles block strings
before normal strings to avoid partial matches.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant