Skip to content

Conversation

Agent-Hellboy
Copy link
Owner

@Agent-Hellboy Agent-Hellboy commented Sep 2, 2025

fixes #70

Summary by CodeRabbit

  • New Features

    • JSON Schema–driven fuzz data generation and runtime invariant checks; invariant findings now appear in fuzz results. Enhanced streaming transports and single‑URL transport selection; added stdio compatibility hooks for testing.
  • Tests

    • Large new unit/integration suites for strategies, invariants, transports, and executor; automated test-marker script and component-based test selection in pytest.
  • CI/CD

    • Manual, component-focused test workflow producing per-component coverage artifacts and conditional Codecov uploads; CI workflow inputs and concurrency updated.
  • Documentation

    • Expanded test README and architecture/reference docs.
  • Chores

    • Added pytest markers and made test invocation configurable (tox/pytest).

- Implement comprehensive JSON Schema parser with support for types, constraints, and combinations
- Add property-based invariants for response validation and error checking
- Update documentation with schema parser and invariants details
- Fix tests and linting issues

Fixes #10: Add property-based invariants and checks
Fixes #12: Enhance JSON Schema parsing for fuzz strategies
Fixes #53: Type safety and code quality enhancements
Copy link
Contributor

coderabbitai bot commented Sep 2, 2025

Walkthrough

Adds component-aware test tooling and CI workflows, a JSON Schema-based strategy parser, runtime invariant checks integrated into the fuzz engine, streaming transport APIs and factory URL support, many new unit/integration tests and test helpers, plus related typing and executor shutdown behavior.

Changes

Cohort / File(s) Summary
CI Workflows
**/.github/workflows/component-tests.yml**, **/.github/workflows/tests.yml**
Adds a manual component-tests workflow with per-component gates, coverage collection and conditional Codecov upload; updates tests.yml with workflow_dispatch inputs, permissions, concurrency, fetch-depth, pip cache, coverage-enabled pytest invocation and Codecov gating.
Pytest & Tox config
pyproject.toml, tox.ini
Adds pytest markers to pyproject.toml; makes tox testenv pytest invocation accept {posargs} with default coverage args.
Test selection & helpers
tests/conftest.py, tests/add_markers.py
Adds --component and --changed-only options, marker registration, git-diff-based component detection, collection filtering logic, and a script to inject/merge pytest markers into test files.
Test docs
tests/README.md
New README describing test layout, marker usage, component runs, changed-only flow, and contributor guidance.
New & updated tests
tests/**, tests/unit/**, tests/integration/**
Large additions of unit and integration tests (invariants, schema parser, strategies, executor, safety, transport, many transport tests); numerous files receive whitespace/formatting adjustments.
Schema parser & strategies
mcp_fuzzer/fuzz_engine/strategy/schema_parser.py, .../realistic/tool_strategy.py, .../aggressive/tool_strategy.py
New JSON Schema-driven fuzz-data generator make_fuzz_strategy_from_jsonschema; realistic/aggressive strategies now invoke parser with fallbacks and enhanced generation/fallback logic.
Invariants & fuzzer integration
mcp_fuzzer/fuzz_engine/invariants.py, mcp_fuzzer/fuzz_engine/fuzzer/protocol_fuzzer.py, mcp_fuzzer/types.py
New invariants module (InvariantViolation, verify_response_invariants, verify_batch_responses, helpers); ProtocolFuzzer integrates invariant checks, supports list/dict server responses, and adds invariant_violations to fuzz results; types updated.
Transport API & implementations
mcp_fuzzer/transport/base.py, mcp_fuzzer/transport/http.py, mcp_fuzzer/transport/sse.py, mcp_fuzzer/transport/stdio.py, mcp_fuzzer/transport/streamable_http.py, mcp_fuzzer/transport/factory.py
Adds connect/disconnect/stream_request abstractions; implements _stream_request for HTTP/SSE/StreamableHTTP/Stdio, SSE parsing helpers, stdio compatibility reads/writes, and factory now accepts single URL or legacy protocol+endpoint.
Executor shutdown
mcp_fuzzer/fuzz_engine/executor.py
Avoids double semaphore acquisition in batch execution; on shutdown timeout cancels outstanding tasks and awaits their completion, then cleans _running_tasks.
Docs
docs/architecture.md, docs/reference.md
Documents Schema Parser and Invariants System and documents new public APIs in strategy/invariants modules.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Dev
  participant GH as GitHub Actions
  participant Job as component-tests job
  participant Repo as repo
  participant Py as pytest
  participant CC as Codecov

  Dev->>GH: workflow_dispatch (components?)
  GH->>Job: start component-tests
  Job->>Repo: checkout (fetch-depth:0)
  Job->>Job: determine per-component booleans from input or enable all
  alt component X enabled
    Job->>Py: run pytest - tests/unit/X -> coverage.X.xml
  end
  Job->>Py: run pytest - integration if any component enabled -> coverage.integration.xml
  Job->>Job: check coverage files & CODECOV_TOKEN
  alt has_token && has_coverage
    Job->>CC: upload coverage.*.xml
  end
Loading
sequenceDiagram
  autonumber
  participant User
  participant Py as pytest
  participant Hook as tests/conftest.py
  participant Git as git

  User->>Py: pytest [--component=a,b | --changed-only]
  Py->>Hook: register options & configure
  alt --changed-only
    Hook->>Git: git diff HEAD -- mcp_fuzzer/*
    Git-->>Hook: changed file paths -> components set
  end
  Hook->>Py: pytest_collection_modifyitems -> skip unit tests not matching selected components (integration exempt)
  Py-->>User: run filtered tests
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Assessment against linked issues

Objective (issue #70) Addressed Explanation
Organise test directory into components (#70)
Tests should run with a flag and only those components with code changes run (#70)
Make component independent (#70) Conftest implements marker-based filtering and changed-only detection but does not enforce or validate fixture/resource isolation programmatically.
Add integration tests (#70)

Out-of-scope changes

Code Change Explanation
JSON Schema parser added (mcp_fuzzer/fuzz_engine/strategy/schema_parser.py) Feature addition for schema-driven fuzzing; not required by test reorganization objective.
Invariants runtime and ProtocolFuzzer integration (mcp_fuzzer/fuzz_engine/invariants.py, .../protocol_fuzzer.py, mcp_fuzzer/types.py) New runtime invariant checks and result-shape changes extend fuzzing functionality beyond test reorg goals.
Transport streaming and factory URL parsing (mcp_fuzzer/transport/*.py, mcp_fuzzer/transport/factory.py) Streaming implementations and URL-based factory selection are functional feature additions unrelated to organizing tests.

Possibly related PRs

Poem

I hop through tunnels, markers in hand,
I flag the tests across the land.
CI wakes softly at my call,
Only changed components heed the hall.
A rabbit cheers — green ticks for all. 🐇

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch reorg_test

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

codecov bot commented Sep 2, 2025

Codecov Report

❌ Patch coverage is 52.54237% with 392 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.36%. Comparing base (fce20aa) to head (027ce10).
⚠️ Report is 24 commits behind head on main.

Files with missing lines Patch % Lines
mcp_fuzzer/fuzz_engine/strategy/schema_parser.py 41.05% 153 Missing and 48 partials ⚠️
mcp_fuzzer/transport/sse.py 29.70% 63 Missing and 8 partials ⚠️
mcp_fuzzer/transport/streamable_http.py 3.44% 28 Missing ⚠️
mcp_fuzzer/fuzz_engine/invariants.py 80.18% 13 Missing and 9 partials ⚠️
mcp_fuzzer/transport/http.py 53.12% 11 Missing and 4 partials ⚠️
...er/fuzz_engine/strategy/realistic/tool_strategy.py 83.33% 7 Missing and 7 partials ⚠️
mcp_fuzzer/transport/stdio.py 60.00% 6 Missing and 6 partials ⚠️
mcp_fuzzer/transport/factory.py 57.69% 9 Missing and 2 partials ⚠️
...r/fuzz_engine/strategy/aggressive/tool_strategy.py 71.87% 6 Missing and 3 partials ⚠️
mcp_fuzzer/fuzz_engine/fuzzer/protocol_fuzzer.py 60.00% 7 Missing and 1 partial ⚠️
... and 1 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #76      +/-   ##
==========================================
- Coverage   60.83%   58.36%   -2.47%     
==========================================
  Files          51       53       +2     
  Lines        3679     4393     +714     
  Branches      542      747     +205     
==========================================
+ Hits         2238     2564     +326     
- Misses       1299     1602     +303     
- Partials      142      227      +85     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

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

⚠️ Outside diff range comments (3)
tests/unit/fuzz_engine/fuzzer/test_tool_fuzzer.py (3)

129-147: Patch async function with AsyncMock; current patch will be awaited and fail.

fuzz_tools awaits self.fuzzer.fuzz_tool, but this patch creates a non-awaitable MagicMock.

Fix by using AsyncMock and an async side_effect:

-        with patch.object(self.fuzzer, "fuzz_tool") as mock_fuzz:
-
-            def side_effect(tool, runs, phase="aggressive"):
+        with patch.object(self.fuzzer, "fuzz_tool", new_callable=AsyncMock) as mock_fuzz:
+
+            async def side_effect(tool, runs, phase="aggressive"):
                 if tool["name"] == "tool1":
                     raise Exception("Test exception")
                 else:
                     return [
                         {
                             "tool_name": tool["name"],
                             "run": 1,
                             "args": {"param2": 42},
                             "success": True,
                         }
                     ]

524-532: Await the coroutines; test currently treats coroutines as results.

fuzz_tool is async; not awaiting leads to TypeError in assertions.

Apply:

-        results1 = fuzzer1.fuzz_tool(tool, runs=1)
-        results2 = fuzzer2.fuzz_tool(tool, runs=1)
+        results1 = await fuzzer1.fuzz_tool(tool, runs=1)
+        results2 = await fuzzer2.fuzz_tool(tool, runs=1)

1-536: Add module-level pytest markers
Insert at the top of tests/unit/fuzz_engine/fuzzer/test_tool_fuzzer.py:

import pytest

pytestmark = [pytest.mark.unit, pytest.mark.fuzz_engine, pytest.mark.fuzzer]
🧹 Nitpick comments (25)
.github/workflows/tests.yml (1)

15-17: Speed up CI and improve signal.

  • Cache pip to cut install time.
  • Consider branch coverage for richer reports.

Apply:

-        with:
-          python-version: '3.11'
+        with:
+          python-version: '3.11'
+          cache: 'pip'
@@
-          pytest -vv --cov=mcp_fuzzer --cov-report=xml
+          pytest -vv --cov=mcp_fuzzer --cov-report=xml --cov-branch

Also applies to: 25-25

tests/unit/safety_system/test_policy.py (1)

1-11: Optional: add explicit component markers here.
If marker selection is used, make it explicit in this module to avoid relying solely on auto-marking.

@@
 #!/usr/bin/env python3
 import os
+import pytest
@@
 from mcp_fuzzer.safety_system.policy import (
@@
 )
 
+pytestmark = [pytest.mark.unit, pytest.mark.safety_system]
tests/unit/config/test_config_loader.py (1)

19-24: Use tmp_path to simplify temp management.
Reduces manual cleanup and flakiness.

-@pytest.fixture
-def config_files():
-    """Create temporary YAML files for testing."""
-    # Create temporary directory
-    temp_dir = tempfile.TemporaryDirectory()
+@pytest.fixture
+def config_files(tmp_path):
+    """Create temporary YAML files for testing."""
@@
-    yml_path = os.path.join(temp_dir.name, "mcp-fuzzer.yml")
-    with open(yml_path, "w") as f:
+    yml_path = os.path.join(tmp_path, "mcp-fuzzer.yml")
+    with open(yml_path, "w") as f:
@@
-    yaml_path = os.path.join(temp_dir.name, "mcp-fuzzer.yaml")
-    with open(yaml_path, "w") as f:
+    yaml_path = os.path.join(tmp_path, "mcp-fuzzer.yaml")
+    with open(yaml_path, "w") as f:
@@
-        "temp_dir": temp_dir,
-        "yml_path": yml_path,
-        "yaml_path": yaml_path,
+        "temp_dir": tmp_path,
+        "yml_path": yml_path,
+        "yaml_path": yaml_path,
     }
-
-    # Cleanup
-    temp_dir.cleanup()

Also applies to: 36-55

tests/unit/fuzz_engine/runtime/test_runtime.py (2)

1-21: Optional: add component markers for selection.
Make runtime/fuzz_engine component intent explicit.

@@
 import pytest
@@
 from mcp_fuzzer.fuzz_engine.runtime import (
@@
 )
 
+pytestmark = [pytest.mark.unit, pytest.mark.fuzz_engine, pytest.mark.runtime]

245-247: Avoid sleep in unit tests.
Replace real sleeps with direct timestamp manipulation to keep tests fast/deterministic.

-            # Wait a small amount of time to ensure timestamp would be different
-            await asyncio.sleep(0.01)
+            # Simulate time passing without real sleep
+            async with watchdog._lock:
+                watchdog._processes[12345]["last_activity"] -= 1.0
pyproject.toml (1)

76-78: Resolve verbosity/coverage defaults split between tox and addopts.

-q in addopts and -vv default in tox will fight. Prefer one source of truth.

Example:

-addopts = "-q --maxfail=1 --disable-warnings --cov=mcp_fuzzer --cov-report=term-missing --cov-report=xml"
+addopts = "--maxfail=1 --disable-warnings --cov=mcp_fuzzer --cov-report=term-missing --cov-report=xml"

Then keep verbosity only in tox or vice versa.

tests/unit/fuzz_engine/fuzzer/test_tool_fuzzer.py (1)

337-357: Minor: also assert run indices map to generated args.

Optional, but strengthens the coupling between i and args.

         for i, result in enumerate(results):
             self.assertIn("success", result)
             self.assertTrue(result["success"], "Expected success=True for each run")
             self.assertIn("args", result)
             # Test that we got the expected arguments from our mock
             args = result["args"]
             self.assertIsInstance(args, dict)
+            self.assertEqual(result["run"], i + 1)
             self.assertEqual(args["name"], f"test_{i}")
             self.assertEqual(args["count"], i)
             self.assertEqual(args["enabled"], i % 2 == 0)
tox.ini (1)

10-21: Align verbosity/coverage with pyproject addopts to avoid conflicting flags.

Currently defaults here (-vv) can conflict with addopts (-q). Pick one place for verbosity.

Example:

-commands =
-    pytest {posargs:-vv --cov=mcp_fuzzer --cov-report=term-missing}
+commands =
+    pytest {posargs}

Let pyproject drive defaults.

tests/README.md (3)

9-24: Add language to fenced code block.

Fixes MD040 warning; use “text” for directory trees.

-```
+```text
 tests/
 ├── unit/                     # Unit tests for individual components
 ...
-```
+```

99-105: Tighten phrasing per grammar hint.

Minor style polish.

-Integration tests typically:
+Integration tests typically do the following:
 - Test the interaction between two or more components
 - Test end-to-end functionality
 - May use real or simulated external services

32-36: Consider showing component-marked examples to reinforce new workflow.

Add -m examples to the “Run all tests”/tox snippets for discoverability.

Example:

 pytest
 # or
 tox -e tests
+# run only fuzz_engine unit tests
+pytest -m "unit and fuzz_engine"
+# or via tox
+tox -e tests -- -m "unit and fuzz_engine"

Also applies to: 41-52, 66-71

tests/unit/fuzz_engine/test_executor.py (1)

182-199: Flakiness risk: timing-based concurrency assertion.

Rare CI jitter could cause >2 starts before first end. Consider asserting semaphore-bound parallelism via injected probe instead of sleeps.

Sketch:

- first_end_idx = next(i for i, event in enumerate(execution_order) if event.endswith("_end"))
- starts_before_any_end = execution_order[:first_end_idx]
- self.assertLessEqual(len(starts_before_any_end), 2)
+ # Assert no more than 2 concurrent “_start” without a matching “_end”
+ in_flight = 0
+ peak = 0
+ for e in execution_order:
+     in_flight += 1 if e.endswith("_start") else -1
+     peak = max(peak, in_flight)
+ self.assertLessEqual(peak, 2)
tests/integration/test_client_transport.py (2)

20-25: Initialization handshake will trigger an extra POST; simplify by skipping it.

Set transport._initialized to True in setUp to focus this test on a single request path.

         self.transport = StreamableHTTPTransport(self.base_url)
         self.client = ProtocolClient(self.transport)
+        # Skip handshake to avoid mocking multiple requests
+        self.transport._initialized = True

42-43: Unnecessary unittest main guard for pytest-run tests.

Remove or replace to avoid accidental direct execution.

-if __name__ == "__main__":
-    unittest.main()
+if __name__ == "__main__":
+    raise SystemExit("Run this module with pytest")
tests/unit/fuzz_engine/runtime/test_manager.py (1)

1-6: Trim unused imports to reduce noise.

signal/time/call appear unused; consider removing to keep tests tidy.

-import asyncio
-import os
-import signal
-import subprocess
-import time
-from unittest.mock import patch, MagicMock, AsyncMock, call
+import asyncio
+import os
+import subprocess
+from unittest.mock import patch, MagicMock, AsyncMock
tests/add_markers.py (3)

59-65: Docstring detection supports only triple double quotes.

Support both '''...''' and """...""" to place imports/markers correctly.

-        module_header_end = re.search(r'""".*?"""\s*', content, re.DOTALL)
+        module_header_end = re.search(r'("""|\'\'\').*?\1\s*', content, re.DOTALL)

117-123: Mirror the broader docstring regex here too.

-            module_header_end = re.search(r'""".*?"""\s*', content, re.DOTALL)
+            module_header_end = re.search(r'("""|\'\'\').*?\1\s*', content, re.DOTALL)

51-55: Idempotency: existing pytestmark may miss new component markers.

Current early-return skips adding missing markers if any pytestmark exists. Optionally merge missing markers instead of returning.

tests/conftest.py (2)

13-17: Register component markers to avoid PytestUnknownMarkWarning.

If not already in pyproject.toml, add them here.

 def pytest_configure(config):
     """Configure pytest with custom component selection logic."""
     config.addinivalue_line("markers", "unit: mark test as a unit test")
     config.addinivalue_line("markers", "integration: mark test as an integration test")
+    for m in [
+        "auth",
+        "cli",
+        "client",
+        "config",
+        "fuzz_engine",
+        "fuzzer",
+        "runtime",
+        "strategy",
+        "safety_system",
+        "transport",
+    ]:
+        config.addinivalue_line("markers", f"{m}: component marker")

33-55: Make changed-only base ref configurable for CI.

Allow overriding the diff base via env to work on detached HEAD builds.

-        try:
-            # Get changed files from git
-            result = subprocess.run(
-                ["git", "diff", "--name-only", "HEAD"],
+        try:
+            # Get changed files from git (allow base override for CI)
+            base = os.environ.get("PYTEST_CHANGED_BASE", "HEAD")
+            result = subprocess.run(
+                ["git", "diff", "--name-only", base],
                 capture_output=True,
                 text=True,
                 check=True,
             )
.github/workflows/component-tests.yml (5)

23-26: Enable pip cache for faster runs.

Leverage setup-python’s built-in caching.

       - name: Set up Python
         uses: actions/setup-python@v5
         with:
           python-version: '3.11'
+          cache: 'pip'

27-32: Consider installing test extras in one shot.

If the project defines a [project.optional-dependencies].test (or extras_require['test']), prefer pip install -e ".[test]" to ensure plugins/tools stay in sync. Otherwise current commands are fine.


6-12: Optional: trigger when test/config files change.

Right now the workflow won’t run if only tests or pytest config change. If desired, expand paths.

     paths:
       - 'mcp_fuzzer/auth/**'
       - 'mcp_fuzzer/cli/**'
       - 'mcp_fuzzer/client/**'
       - 'mcp_fuzzer/fuzz_engine/**'
       - 'mcp_fuzzer/safety_system/**'
       - 'mcp_fuzzer/transport/**'
+      # Optional: ensure component tests run when only tests/config change
+      - 'tests/**'
+      - 'pyproject.toml'
+      - 'tox.ini'

1-5: Optional: cancel in-flight runs on new commits.

Add workflow-level concurrency to save CI minutes.

Add at the top-level (outside jobs):

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

36-45: Use PR SHAs and anchored grep for reliable diffs

  • In .github/workflows/component-tests.yml (lines 36–45), replace
    BASE_BRANCH=${{ github.base_ref }}
    with
    BASE_SHA=${{ github.event.pull_request.base.sha }}
    HEAD_SHA=${{ github.event.pull_request.head.sha }} (baeldung.com)
  • Change each diff invocation to
    git diff --name-only "$BASE_SHA" "$HEAD_SHA"
    instead of origin/$BASE_BRANCH HEAD (github.com)
  • Anchor grep patterns to the path start, e.g.
    grep -q "^mcp_fuzzer/auth/" (phoenixnap.com)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between fce20aa and 1b9d98f.

📒 Files selected for processing (15)
  • .github/workflows/component-tests.yml (1 hunks)
  • .github/workflows/tests.yml (1 hunks)
  • pyproject.toml (1 hunks)
  • tests/README.md (1 hunks)
  • tests/add_markers.py (1 hunks)
  • tests/conftest.py (1 hunks)
  • tests/integration/test_client_transport.py (1 hunks)
  • tests/unit/auth/test_auth.py (2 hunks)
  • tests/unit/config/test_config_loader.py (7 hunks)
  • tests/unit/fuzz_engine/fuzzer/test_tool_fuzzer.py (9 hunks)
  • tests/unit/fuzz_engine/runtime/test_manager.py (33 hunks)
  • tests/unit/fuzz_engine/runtime/test_runtime.py (11 hunks)
  • tests/unit/fuzz_engine/test_executor.py (7 hunks)
  • tests/unit/safety_system/test_policy.py (1 hunks)
  • tox.ini (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
tests/integration/test_client_transport.py (2)
mcp_fuzzer/client/protocol_client.py (1)
  • ProtocolClient (19-399)
mcp_fuzzer/transport/streamable_http.py (1)
  • StreamableHTTPTransport (41-370)
tests/unit/fuzz_engine/fuzzer/test_tool_fuzzer.py (2)
mcp_fuzzer/fuzz_engine/strategy/strategy_manager.py (1)
  • fuzz_tool_arguments (54-61)
mcp_fuzzer/fuzz_engine/fuzzer/tool_fuzzer.py (1)
  • ToolFuzzer (21-257)
tests/unit/safety_system/test_policy.py (1)
mcp_fuzzer/safety_system/policy.py (2)
  • is_host_allowed (69-112)
  • configure_network_policy (26-66)
tests/unit/fuzz_engine/runtime/test_runtime.py (3)
tests/unit/fuzz_engine/runtime/test_manager.py (1)
  • test_start_process_success (27-46)
mcp_fuzzer/fuzz_engine/runtime/watchdog.py (5)
  • WatchdogConfig (21-28)
  • start (43-48)
  • register_process (239-258)
  • unregister_process (260-268)
  • stop (50-57)
mcp_fuzzer/fuzz_engine/runtime/manager.py (3)
  • ProcessManager (35-415)
  • get_process_status (182-198)
  • ProcessConfig (23-32)
tests/unit/fuzz_engine/runtime/test_manager.py (1)
mcp_fuzzer/fuzz_engine/runtime/manager.py (7)
  • start_process (46-95)
  • ProcessConfig (23-32)
  • stop_process (97-127)
  • get_process_status (182-198)
  • list_processes (200-213)
  • wait_for_process (215-233)
  • cleanup_finished_processes (256-274)
tests/unit/config/test_config_loader.py (2)
mcp_fuzzer/config_loader.py (2)
  • find_config_file (20-60)
  • apply_config_file (102-133)
mcp_fuzzer/config.py (1)
  • update (100-102)
tests/unit/fuzz_engine/test_executor.py (1)
mcp_fuzzer/fuzz_engine/executor.py (3)
  • execute_with_retry (97-151)
  • execute_batch (153-199)
  • _execute_and_track (201-218)
🪛 LanguageTool
tests/README.md

[grammar] ~101-~101: There might be a mistake here.
Context: ...n` marker. Integration tests typically: - Test the interaction between two or more...

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
tests/README.md

9-9: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ 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: tests
🔇 Additional comments (14)
tests/unit/auth/test_auth.py (1)

12-12: LGTM on pytest import.
Import is correct and keeps deps minimal.

.github/workflows/tests.yml (1)

25-25: Coverage XML generation looks good.
Command will emit ./coverage.xml for Codecov. No issues.

tests/unit/safety_system/test_policy.py (1)

52-62: Whitespace-only change — OK.
No functional impact; tests remain readable.

tests/unit/config/test_config_loader.py (1)

24-56: Formatting-only edits — OK.
No behavioral changes detected; tests still exercise config discovery and application paths.

Also applies to: 62-77, 99-107, 116-124, 126-134, 136-151, 152-159

tests/unit/fuzz_engine/runtime/test_runtime.py (1)

26-48: Track and unskip start_process test.
The skip is reasonable short-term; please add an issue reference or TODO with a target date to unskip. When ready, patch asyncio.create_subprocess_exec and assert register_process was called.

Would you like me to draft a minimal unskipped test using AsyncMock for create_subprocess_exec and asserting watchdog.register_process interactions?

tests/unit/fuzz_engine/fuzzer/test_tool_fuzzer.py (3)

161-164: LGTM on safety patching scope.

Patching is_safe_tool_call at the module path is correct for isolating schema-focused behavior.


182-190: LGTM on deterministic sanitize mock.

Predictable sanitized args make assertions robust here.


315-321: Nice deterministic inputs for “different runs” test.

Good use of side_effect sequence to assert variability.

tox.ini (1)

20-20: LGTM: posargs passthrough with sensible defaults.

This makes CI/local overrides easier while preserving coverage defaults.

tests/unit/fuzz_engine/test_executor.py (2)

93-107: LGTM: retries test accurately asserts attempts = retries + 1.

Clear coverage of “all fail” path.


218-234: LGTM: shutdown covers cancellation path and leaves only completed tasks tracked.

Good coverage of graceful shutdown with running tasks.

tests/unit/fuzz_engine/runtime/test_manager.py (2)

26-47: Process start path test looks solid.

Mocks and assertions align with start_process: watchdog.start, register_process, and status “running” are verified.


349-373: Timeout handling assertion matches manager.wait_for_process semantics.

Asserting None on timeout is consistent with returning current returncode when asyncio.TimeoutError is raised.

tests/conftest.py (1)

71-76: Skip logic is clear and minimal. LGTM.

Comment on lines 37 to 45
markers = []
for path_prefix, path_markers in COMPONENT_MARKERS.items():
if parent_dir.startswith(path_prefix):
markers = path_markers
break

if not markers:
print(f"Could not determine markers for {file_path}")
return False
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Prefix shadowing: choose the most specific (longest) directory match.

As written, “unit/fuzz_engine” preempts “unit/fuzz_engine/runtime”. Pick the longest matching prefix to apply runtime/fuzzer/strategy markers correctly.

-    markers = []
-    for path_prefix, path_markers in COMPONENT_MARKERS.items():
-        if parent_dir.startswith(path_prefix):
-            markers = path_markers
-            break
+    markers = []
+    # Choose the most specific (longest) matching prefix
+    best_prefix = None
+    for path_prefix in COMPONENT_MARKERS.keys():
+        if parent_dir.startswith(path_prefix) and (
+            best_prefix is None or len(path_prefix) > len(best_prefix)
+        ):
+            best_prefix = path_prefix
+    if best_prefix:
+        markers = COMPONENT_MARKERS[best_prefix]
📝 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
markers = []
for path_prefix, path_markers in COMPONENT_MARKERS.items():
if parent_dir.startswith(path_prefix):
markers = path_markers
break
if not markers:
print(f"Could not determine markers for {file_path}")
return False
markers = []
# Choose the most specific (longest) matching prefix
best_prefix = None
for path_prefix in COMPONENT_MARKERS.keys():
if parent_dir.startswith(path_prefix) and (
best_prefix is None or len(path_prefix) > len(best_prefix)
):
best_prefix = path_prefix
if best_prefix:
markers = COMPONENT_MARKERS[best_prefix]
if not markers:
print(f"Could not determine markers for {file_path}")
return False
🤖 Prompt for AI Agents
In tests/add_markers.py around lines 37 to 45, the loop picks the first
path_prefix match which causes shorter prefixes (e.g. "unit/fuzz_engine") to
shadow longer, more specific ones ("unit/fuzz_engine/runtime"); change the logic
to find the longest matching prefix: iterate all COMPONENT_MARKERS keys, for
each if parent_dir.startswith(path_prefix) check its length and keep the
path_markers from the longest matching prefix seen so far (track best_length),
then after the loop use that best match's markers (leave the existing "if not
markers" error path intact).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
tests/integration/test_client_transport.py (1)

6-13: Resolved: missing httpx/json imports from prior review.

The added imports address the earlier failures and align with the mocking strategy.

🧹 Nitpick comments (4)
tests/integration/test_client_transport.py (4)

33-37: Optionally attach a Request to the mocked Response for richer diagnostics.

Not required for success, but setting Response.request improves error messages if the test ever fails on raise_for_status().

         mock_post.return_value = httpx.Response(
             200,
             headers={"Content-Type": "application/json"},
-            content=b'{"jsonrpc":"2.0","id":"1","result":"test"}',
+            content=b'{"jsonrpc":"2.0","id":"1","result":"test"}',
+            request=httpx.Request("POST", self.base_url),
         )

6-12: Trim unused imports (nit).

asyncio, json, and MagicMock aren’t used.

-import asyncio
-import json
 import unittest
-from unittest.mock import MagicMock, patch
+from unittest.mock import patch
@@
 import httpx
 import pytest

1-1: Shebang is unnecessary in test modules (nit).

Tests are executed by pytest, not as executables.

-#!/usr/bin/env python3

49-50: Drop the unittest main guard (nit).

Pytest will discover and run the test; this block is redundant and can confuse execution paths.

-if __name__ == "__main__":
-    unittest.main()
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1b9d98f and 021501b.

📒 Files selected for processing (6)
  • .github/workflows/component-tests.yml (1 hunks)
  • pyproject.toml (1 hunks)
  • tests/add_markers.py (1 hunks)
  • tests/conftest.py (1 hunks)
  • tests/integration/test_client_transport.py (1 hunks)
  • tests/unit/auth/test_auth.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • pyproject.toml
  • tests/unit/auth/test_auth.py
  • tests/add_markers.py
  • tests/conftest.py
  • .github/workflows/component-tests.yml
🧰 Additional context used
🧬 Code graph analysis (1)
tests/integration/test_client_transport.py (1)
mcp_fuzzer/client/protocol_client.py (1)
  • _send_generic_request (389-395)
🔇 Additional comments (1)
tests/integration/test_client_transport.py (1)

16-16: Good use of component markers for selective runs.

This aligns with the PR goal to organize and run tests per component.

Comment on lines 44 to 46
# Verify the transport was used correctly
mock_post.assert_called_once()
self.assertEqual(result, {"result": "test"})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Account for the implicit initialize handshake: expect two POSTs and assert methods.

StreamableHTTPTransport performs an initialize before non-initialize calls, so post() is invoked twice. The current assert_called_once() will fail nondeterministically.

-        mock_post.assert_called_once()
+        # initialize + the actual request
+        self.assertEqual(mock_post.call_count, 2)
+        first_call, second_call = mock_post.call_args_list
+        self.assertEqual(first_call.kwargs["json"]["method"], "initialize")
+        self.assertEqual(second_call.kwargs["json"]["method"], "test_method")
         self.assertEqual(result, {"result": "test"})
📝 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
# Verify the transport was used correctly
mock_post.assert_called_once()
self.assertEqual(result, {"result": "test"})
# Verify the transport was used correctly
# initialize + the actual request
self.assertEqual(mock_post.call_count, 2)
first_call, second_call = mock_post.call_args_list
self.assertEqual(first_call.kwargs["json"]["method"], "initialize")
self.assertEqual(second_call.kwargs["json"]["method"], "test_method")
self.assertEqual(result, {"result": "test"})
🤖 Prompt for AI Agents
In tests/integration/test_client_transport.py around lines 44-46, the test
incorrectly assumes only one POST call even though StreamableHTTPTransport
performs an initialize handshake and will call post() twice; update the
assertions to expect two POSTs (e.g., assert call_count == 2) and replace
assert_called_once() with assertions that verify both POSTs occurred and their
order/arguments as needed (use call_count and assert_has_calls or inspect
mock_post.call_args_list to check the first is the init handshake and the second
is the actual request), then keep the result equality assertion.

Copy link

github-actions bot commented Sep 3, 2025

Documentation Preview

Your documentation changes have been built successfully!

Preview URL: https://Agent-Hellboy.github.io/mcp-server-fuzzer/

The documentation will be automatically deployed to the main site when this PR is merged.


This comment was automatically generated by the documentation workflow.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

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

⚠️ Outside diff range comments (4)
mcp_fuzzer/fuzz_engine/fuzzer/protocol_fuzzer.py (3)

309-313: Type hint mismatch: server_response can be a list.

_send_fuzzed_request may return a list (batch) but the annotation claims Dict[str, Any]. This leaks into callers and TypedDicts and will trip static analysis.

Apply:

-    ) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
+    ) -> Tuple[Optional[Union[Dict[str, Any], List[Any]]], Optional[str]]:

347-349: Align create_fuzz_result signature with batch responses.

server_response is documented/used as either dict or list. Update annotation accordingly.

Apply:

-        server_response: Optional[Dict[str, Any]],
+        server_response: Optional[Union[Dict[str, Any], List[Any]]],

342-372: Add missing TypedDict keys for FuzzDataResult
In mcp_fuzzer/types.py, FuzzDataResult currently omits invariant_violations (and passed_invariants if you choose to track it), yet both are written into the result in protocol_fuzzer.py. Extend the TypedDict with:

class FuzzDataResult(TypedDict, total=False):
    …
    invariant_violations: List[str]
    passed_invariants:   List[str]  # add only if used
docs/architecture.md (1)

321-339: Duplicate section numbering (two “### 6”).

“Authentication System” is numbered 6 but follows “Safety System” (also 6). Renumber to keep sequence consistent.

Apply:

-### 6. Authentication System
+### 7. Authentication System
@@
-### 7. Reporting System
+### 8. Reporting System
🧹 Nitpick comments (36)
mcp_fuzzer/fuzz_engine/strategy/schema_parser.py (8)

32-49: allOf merge is shallow; merge type constraints too.

Merging only properties/required may yield invalid composites. Intersect type constraints across subschemas.

 def _merge_allOf(schemas: List[Dict[str, Any]]) -> Dict[str, Any]:
     """Deep merge for allOf schemas."""
     merged: Dict[str, Any] = {}
     props: Dict[str, Any] = {}
     required: List[str] = []
+    merged_types: "set[str] | None" = None
     for s in schemas:
         if "properties" in s and isinstance(s["properties"], dict):
             props.update(s["properties"])
         if "required" in s and isinstance(s["required"], list):
             required.extend([r for r in s["required"] if isinstance(r, str)])
+        t = s.get("type")
+        if t:
+            tset = set(t if isinstance(t, list) else [t])
+            merged_types = tset if merged_types is None else (merged_types & tset)
         for k, v in s.items():
             if k not in ("properties", "required"):
                 merged[k] = v if k not in merged else merged[k]
     if props:
         merged["properties"] = props
     if required:
         merged["required"] = sorted(set(required))
+    if merged_types:
+        merged["type"] = list(merged_types) if len(merged_types) > 1 else next(iter(merged_types))
     return merged

68-71: Type-aware recursion cutoff.

Returning {} or [] blindly may violate schema types. Make the guard respect the declared type.

-    # Prevent excessive recursion
-    if recursion_depth > MAX_RECURSION_DEPTH:
-        return {} if random.random() < 0.5 else []
+    # Prevent excessive recursion
+    if recursion_depth > MAX_RECURSION_DEPTH:
+        t = schema.get("type")
+        return {
+            "object": {},
+            "array": [],
+            "string": "",
+            "integer": 0,
+            "number": 0.0,
+            "boolean": False,
+            "null": None,
+        }.get(t, None)

171-178: Honor property defaults in realistic mode.

If a property schema defines "default", prefer it sometimes to improve validity.

-    for prop_name, prop_schema in properties.items():
+    for prop_name, prop_schema in properties.items():
         # For required properties or by chance for optional ones
         if prop_name in required or random.random() < 0.8:
-            result[prop_name] = make_fuzz_strategy_from_jsonschema(
-                prop_schema, phase, recursion_depth + 1
-            )
+            if phase == "realistic" and "default" in prop_schema and random.random() < 0.5:
+                result[prop_name] = prop_schema["default"]
+            else:
+                result[prop_name] = make_fuzz_strategy_from_jsonschema(
+                    prop_schema, phase, recursion_depth + 1
+                )

188-199: JS prototype-pollution keys — intentional? Gate behind a flag.

Injecting "proto"/"constructor" is useful for security fuzzing, but can corrupt downstream JS consumers. Consider a toggle (e.g., aggressive_js_object_keys=True) or a marker to enable/disable.


205-215: Add tuple validation support for items as a list.

JSON Schema allows items to be an array (positional schemas). Minimal support improves coverage.

 def _handle_array_type(
@@
-    items_schema = schema.get("items", {})
+    items_schema = schema.get("items", {})
@@
-    # If this is an array property without items specification,
+    # Tuple validation: items is a list of schemas
+    if isinstance(items_schema, list):
+        return [
+            make_fuzz_strategy_from_jsonschema(sub, phase, recursion_depth + 1)
+            for sub in items_schema
+        ]
+
+    # If this is an array property without items specification,
     # generate an array of simple values
     if not items_schema:
         return [_generate_default_value(phase) for _ in range(random.randint(1, 3))]

Also applies to: 246-251


23-27: uniqueItems handling: use a stable JSON hash, not str(item).

str() can collide and misses structural differences. Use json.dumps(sort_keys=True).

@@
-import random
+import random
+import json
@@
-                item_hash = str(item)
+                item_hash = json.dumps(item, sort_keys=True, default=str)
                 attempts = 0
-                while item_hash in seen_values and attempts < 10:
+                while item_hash in seen_values and attempts < 10:
                     item = make_fuzz_strategy_from_jsonschema(
                         items_schema, phase, recursion_depth + 1
                     )
-                    item_hash = str(item)
+                    item_hash = json.dumps(item, sort_keys=True, default=str)
                     attempts += 1
                 seen_values.add(item_hash)

Also applies to: 256-265


575-605: Aggressive numbers: sometimes return NaN/Inf even if out-of-bounds.

Current range-filtering drops NaN/Inf. Keep a small chance to emit them to exercise parsers.

     else:
         # In aggressive mode, sometimes use edge cases
-        if random.random() < 0.7:
+        if random.random() < 0.7:
             # Normal value
             return random.uniform(minimum, maximum)
         else:
             # Edge cases
-            edge_cases = [
+            # 25%: special IEEE-754 values irrespective of bounds
+            if random.random() < 0.25:
+                return random.choice([float("inf"), float("-inf"), float("nan")])
+            edge_cases = [
                 minimum,  # Minimum value
                 maximum,  # Maximum value
                 0.0,  # Zero
                 -0.0,  # Negative zero
-                float("inf"),  # Infinity
-                float("-inf"),  # Negative infinity
-                float("nan"),  # Not a number
             ]

284-325: Regex pattern generation is naive — document limitations or plug a generator.

Consider integrating exrex/rstr/graph-based generators under an optional dependency to better satisfy complex patterns; otherwise, document this limitation in docs.

mcp_fuzzer/fuzz_engine/invariants.py (3)

85-96: Requests vs. responses: enforce expectations or rename API.

check_response_validity currently accepts request objects (has method and id) as “valid responses.” Either reject them or rename to reflect “envelope validity.”

-            # Determine if this is a notification, request, or response
+            # Determine if this is a notification, request, or response
@@
-            # If it has id but no method, it's a response and needs result or error
+            # If it has id but no method, it's a response and needs result or error
             if not has_result and not has_error and has_id and not has_method:
                 # Responses must have result or error
                 raise InvariantViolation(
                     "JSON-RPC response must have either 'result' or 'error'",
                     response,
                 )
+            # If it has both method and id, treat as a request; this checker expects responses
+            if has_method and has_id and not (has_result or has_error):
+                raise InvariantViolation(
+                    "Received a JSON-RPC request where a response was expected", response
+                )

211-213: jsonschema optional dependency — make desired behavior explicit.

Currently we warn and return True. Consider a strict flag to fail fast when schema checks are requested but jsonschema isn’t installed.

Would you like a PR follow-up adding a strict_schema_validation toggle to raise instead of warn?


324-345: State consistency: allow explicit additions/removals lists.

Support expected_additions/expected_removals to model create/delete flows without overloading expected_changes.

docs/reference.md (2)

188-197: Document JSON-RPC strictness and limitations.

Mention that invariants treat non-JSON-RPC dicts as invalid and require id/result|error for responses; add a note on optional jsonschema dependency.


528-536: Avoid real-looking secret prefixes in examples.

Use neutral placeholders to prevent scanners from flagging docs.

-      "api_key": "sk-your-openai-api-key",
+      "api_key": "YOUR_OPENAI_API_KEY",
@@
-      "api_key": "ghp-your-github-token",
+      "api_key": "YOUR_GITHUB_TOKEN",
mcp_fuzzer/fuzz_engine/fuzzer/protocol_fuzzer.py (2)

220-252: Tighten batch validation call and make timeout a constant.

Minor cleanups:

  • Avoid creating an intermediate task; wait_for can take the coroutine directly.
  • Hoist the 5s literal into a named constant for configurability.

Apply:

@@
-                        try:
-                            # Handle batch responses with timeout to prevent hanging
-                            batch = await asyncio.wait_for(
-                                asyncio.create_task(
-                                    verify_batch_responses(server_response)
-                                ),
-                                timeout=5.0,  # 5 second timeout for batch validation
-                            )
+                        try:
+                            # Handle batch responses with timeout to prevent hanging
+                            batch = await asyncio.wait_for(
+                                verify_batch_responses(server_response),
+                                timeout=self.BATCH_VALIDATION_TIMEOUT,
+                            )

And near the class constants (after PROTOCOL_TYPES), add:

@@ class ProtocolFuzzer:
     PROTOCOL_TYPES: ClassVar[Tuple[str, ...]] = (
       "InitializeRequest",
       ...
       "CompleteRequest",
     )
+    # Seconds to wait for invariant validation of batch responses
+    BATCH_VALIDATION_TIMEOUT: ClassVar[float] = 5.0

363-372: Clarify success semantics vs invariant failures.

Currently success reflects transport acceptance only. Consider exposing a derived flag to simplify consumers.

Apply:

         return {
             "protocol_type": protocol_type,
             "run": run_index + 1,
             "fuzz_data": fuzz_data,
             "success": server_error is None,
             "server_response": server_response,
             "server_error": server_error,
             "server_rejected_input": server_error is not None,
             "invariant_violations": [],  # Will be populated if violations occur
+            # Derived convenience flag for report filtering
+            # "passed_invariants" will be backfilled by caller if violations found
+            "passed_invariants": True,
         }

And when setting violations:

-            if invariant_violations:
-                result["invariant_violations"] = invariant_violations
+            if invariant_violations:
+                result["invariant_violations"] = invariant_violations
+                result["passed_invariants"] = False
docs/architecture.md (3)

247-258: Minor wording polish in Fuzzing Engine bullets and flow steps.

Tighten phrasing and fix small grammar nits.

Apply:

-- `invariants.py`: Implements property-based invariants and checks for fuzz testing
+- `invariants.py`: Implements property-based invariants and checks for fuzz testing.
@@
-5. **Invariant Verification**: Verify responses against property-based invariants
-6. **Analysis**: Analyze results and generate reports
+5. **Invariant Verification**: Verify responses against property-based invariants.
+6. **Analysis**: Analyze results and generate reports.

275-288: Capitalize “JSON Schema” and fix list punctuation.

Improve consistency and readability.

Apply:

-The schema parser provides comprehensive support for parsing JSON Schema definitions and generating appropriate test data based on schema specifications. It handles:
+The schema parser provides comprehensive support for parsing JSON Schema definitions and generating appropriate test data based on schema specifications. It handles:
@@
-- Basic types: string, number, integer, boolean, array, object, null
-- String constraints: minLength, maxLength, pattern, format
-- Number/Integer constraints: minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf
-- Array constraints: minItems, maxItems, uniqueItems
-- Object constraints: required properties, minProperties, maxProperties
-- Schema combinations: oneOf, anyOf, allOf
-- Enums and constants
+- Basic types: string, number, integer, boolean, array, object, null.
+- String constraints: minLength, maxLength, pattern, format.
+- Number/integer constraints: minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf.
+- Array constraints: minItems, maxItems, uniqueItems.
+- Object constraints: required properties, minProperties, maxProperties.
+- Schema combinations: oneOf, anyOf, allOf.
+- Enums and constants.

291-303: Invariants System bullets: grammar consistency.

Add articles and end punctuation.

Apply:

-- `check_response_validity`: Ensures responses follow JSON-RPC 2.0 specification
-- `check_error_type_correctness`: Verifies error responses have correct structure and codes
-- `check_response_schema_conformity`: Validates responses against JSON schema definitions
-- `verify_response_invariants`: Orchestrates multiple invariant checks on a single response
-- `verify_batch_responses`: Applies invariant checks to batches of responses
-- `check_state_consistency`: Ensures server state remains consistent during fuzzing
+- `check_response_validity`: Ensures responses follow the JSON-RPC 2.0 specification.
+- `check_error_type_correctness`: Verifies that error responses have the correct structure and codes.
+- `check_response_schema_conformity`: Validates responses against JSON Schema definitions.
+- `verify_response_invariants`: Orchestrates multiple invariant checks on a single response.
+- `verify_batch_responses`: Applies invariant checks to batches of responses.
+- `check_state_consistency`: Ensures server state remains consistent during fuzzing.
tests/test_schema_parser.py (5)

10-19: Avoid testing private helpers directly where possible.

Importing underscored functions ties tests to internals and increases churn. Prefer exercising behavior via make_fuzz_strategy_from_jsonschema; keep a minimal smoke test on internals only where the public path can’t cover it.


25-53: Seed randomness for stability (optional).

Many assertions check ranges/types and are fine, but seeding reduces flaky edge cases across environments.

Apply:

@@
-import unittest
+import unittest
+import random
@@
-class TestSchemaParser(unittest.TestCase):
+class TestSchemaParser(unittest.TestCase):
     """Test cases for schema_parser.py - BEHAVIOR focused."""
 
+    @classmethod
+    def setUpClass(cls):
+        random.seed(12345)

144-154: Stronger URI validation.

Instead of startswith checks, validate scheme via urlparse to avoid false positives.

Apply:

@@
-        self.assertTrue(
-            uri_result.startswith("http://") or uri_result.startswith("https://"),
-            "URI should start with http:// or https://",
-        )
+        from urllib.parse import urlparse
+        self.assertIn(urlparse(uri_result).scheme, ("http", "https"), "Invalid URI scheme")

291-400: Strengthen format validations in complex schema.

Add concrete checks for UUID and RFC3339 date-time when present.

Apply:

@@
-        # Check id format
-        self.assertIsInstance(result["id"], str, "id should be a string")
+        # Check id format (UUID-like)
+        self.assertIsInstance(result["id"], str, "id should be a string")
+        try:
+            import uuid as _uuid
+            _ = _uuid.UUID(result["id"])
+        except Exception:
+            self.fail("id is not a valid UUID string")
@@
-            if "created" in result["metadata"]:
-                self.assertIsInstance(
-                    result["metadata"]["created"], str, "created should be a string"
-                )
+            if "created" in result["metadata"]:
+                self.assertIsInstance(result["metadata"]["created"], str, "created should be a string")
+                # Best-effort ISO 8601/RFC3339 validation
+                from datetime import datetime
+                try:
+                    datetime.fromisoformat(result["metadata"]["created"].replace("Z", "+00:00"))
+                except Exception:
+                    self.fail("created is not a valid date-time string")

401-403: Add coverage for multipleOf constraints.

Covers common schema constraint paths for both integers and numbers.

Apply:

@@
 if __name__ == "__main__":
-    unittest.main()
+    unittest.main()
+
+class TestSchemaParserConstraints(unittest.TestCase):
+    def test_integer_multiple_of(self):
+        schema = {"type": "integer", "minimum": 0, "maximum": 100, "multipleOf": 7}
+        val = _handle_integer_type(schema, phase="realistic")
+        self.assertIsInstance(val, int)
+        self.assertEqual(val % 7, 0)
+        self.assertGreaterEqual(val, 0)
+        self.assertLessEqual(val, 100)
+
+    def test_number_multiple_of(self):
+        schema = {"type": "number", "minimum": 0.0, "maximum": 10.0, "multipleOf": 2.5}
+        val = _handle_number_type(schema, phase="realistic")
+        self.assertTrue(isinstance(val, (int, float)))
+        # Use tolerance due to float
+        self.assertLess(abs((val / 2.5) - round(val / 2.5)), 1e-9)
+        self.assertGreaterEqual(val, 0.0)
+        self.assertLessEqual(val, 10.0)
mcp_fuzzer/fuzz_engine/strategy/realistic/tool_strategy.py (3)

149-157: Don't swallow schema-parser errors silently; at least log for observability.

Catching a blanket Exception and defaulting to {} obscures real parser defects and makes failures hard to diagnose in CI.

Consider:

-    try:
-        args = make_fuzz_strategy_from_jsonschema(schema, phase="realistic")
-    except Exception:
-        args = {}
+    try:
+        args = make_fuzz_strategy_from_jsonschema(schema, phase="realistic")
+    except Exception as e:
+        # Optional: add module-level logger if not already present
+        # logger.debug("Schema parser failed in realistic phase: %s", e)
+        args = {}

204-213: Respect exclusiveMinimum/Maximum and multipleOf when present.

Current integer/number generation ignores these common JSON Schema constraints.

Example patch for integers:

-                minimum = prop_spec.get("minimum", -100)
-                maximum = prop_spec.get("maximum", 100)
-                args[prop_name] = random.randint(minimum, maximum)
+                minimum = prop_spec.get("minimum", -100)
+                maximum = prop_spec.get("maximum", 100)
+                if prop_spec.get("exclusiveMinimum") is True:
+                    minimum += 1
+                if prop_spec.get("exclusiveMaximum") is True:
+                    maximum -= 1
+                step = prop_spec.get("multipleOf")
+                val = random.randint(minimum, maximum)
+                if isinstance(step, int) and step > 0:
+                    val = minimum + ((val - minimum) // step) * step
+                args[prop_name] = val

Similar logic can be applied to numbers using uniform() and snapping to multipleOf.


233-236: Emit RFC 3339 UTC timestamps with trailing 'Z' to match docstring.

Docstring says “ending with Z” but isoformat() yields “+00:00”.

Apply:

-                elif format_type == "date-time":
-                    args[prop_name] = datetime.now(timezone.utc).isoformat()
+                elif format_type == "date-time":
+                    args[prop_name] = (
+                        datetime.now(timezone.utc)
+                        .isoformat(timespec="seconds")
+                        .replace("+00:00", "Z")
+                    )
tests/test_invariants.py (1)

6-18: Add component markers to integrate with component-based test selection.

Aligns with the PR goal to organize and run tests per component.

Add at module top (after imports):

+pytestmark = [pytest.mark.unit, pytest.mark.fuzz_engine]
mcp_fuzzer/fuzz_engine/strategy/aggressive/tool_strategy.py (1)

256-286: Optionally backfill required fields even in aggressive mode to reach deeper code paths.

Including required keys (with intentionally malformed values) improves coverage while preserving “aggressive” intent.

Example:

     if not args and schema.get("properties"):
         # Fallback to basic property handling
         properties = schema.get("properties", {})
 
         for prop_name, prop_spec in properties.items():
             if random.random() < 0.8:  # 80% chance to include each property
                 prop_type = prop_spec.get("type", "string")
@@
             else:
                 args[prop_name] = generate_aggressive_text()
+
+    # Ensure required keys exist (values may still be adversarial)
+    required = schema.get("required", [])
+    for key in required or []:
+        if key not in args:
+            args[key] = generate_aggressive_text()
tests/unit/fuzz_engine/strategy/test_realistic_strategies.py (8)

7-14: Add missing import for URI validation (used in assertions below).

You'll need urlparse for robust URI checks.

 import uuid
-from datetime import datetime
+from datetime import datetime
+from urllib.parse import urlparse

32-44: Strengthen Base64 validation (use strict decoder).

Use validate=True to reject non-Base64 chars and catch padding issues.

-        decoded = base64.b64decode(value)
+        decoded = base64.b64decode(value, validate=True)

72-84: Also assert UTC offset is zero.

Confirms timestamps are truly UTC, not just “timezone present”.

         parsed_dt = datetime.fromisoformat(value)
         assert isinstance(parsed_dt, datetime)
         # Should have timezone info
         assert parsed_dt.tzinfo is not None
+        # And it must be UTC
+        assert parsed_dt.utcoffset() is not None and parsed_dt.utcoffset().total_seconds() == 0

149-155: Check microseconds semantically, not by string search.

Parsing avoids false positives due to formatting differences.

-    # Should not contain microseconds (no .)
-    assert "." not in value
+    parsed = datetime.fromisoformat(value)
+    assert parsed.microsecond == 0

232-252: Also assert “scores” array element types.

You define a number-typed array but don’t validate it. Add checks to ensure numeric types.

If this fails, production code likely doesn’t honor item type for arrays; see follow-up verification script below.

     assert isinstance(result["numbers"], list)
     assert 1 <= len(result["numbers"]) <= 3
     assert all(isinstance(num, int) for num in result["numbers"])
 
+    assert isinstance(result["scores"], list)
+    assert 1 <= len(result["scores"]) <= 3
+    assert all(isinstance(s, (int, float)) for s in result["scores"])

287-291: Fix tautological uniqueness check.

len(set(texts)) >= 1 is always true. Require at least two distinct samples.

-    assert len(set(texts)) >= 1
+    assert len(set(texts)) > 1

295-297: Remove redundant re-imports inside tests.

These functions are already imported at module level; local re-imports add noise.

-    from mcp_fuzzer.fuzz_engine.strategy.realistic.tool_strategy import (
-        base64_strings,
-    )
+    # base64_strings imported at module level
-    from mcp_fuzzer.fuzz_engine.strategy.realistic.tool_strategy import (
-        uuid_strings,
-    )
+    # uuid_strings imported at module level
-    from mcp_fuzzer.fuzz_engine.strategy.realistic.tool_strategy import (
-        timestamp_strings,
-    )
+    # timestamp_strings imported at module level

Also applies to: 318-319, 352-354


309-313: Alphabet parameter is a no-op in production; either remove this sub-test or update generator to honor it.

As written, base64_strings ignores alphabet; this test implies otherwise.

Would you prefer I open a follow-up PR to make base64_strings use the alphabet for pre-encoding data?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 021501b and c11b082.

📒 Files selected for processing (10)
  • docs/architecture.md (3 hunks)
  • docs/reference.md (2 hunks)
  • mcp_fuzzer/fuzz_engine/fuzzer/protocol_fuzzer.py (3 hunks)
  • mcp_fuzzer/fuzz_engine/invariants.py (1 hunks)
  • mcp_fuzzer/fuzz_engine/strategy/aggressive/tool_strategy.py (1 hunks)
  • mcp_fuzzer/fuzz_engine/strategy/realistic/tool_strategy.py (1 hunks)
  • mcp_fuzzer/fuzz_engine/strategy/schema_parser.py (1 hunks)
  • tests/test_invariants.py (1 hunks)
  • tests/test_schema_parser.py (1 hunks)
  • tests/unit/fuzz_engine/strategy/test_realistic_strategies.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
tests/test_schema_parser.py (1)
mcp_fuzzer/fuzz_engine/strategy/schema_parser.py (8)
  • make_fuzz_strategy_from_jsonschema (52-128)
  • _handle_enum (131-153)
  • _handle_string_type (274-324)
  • _handle_integer_type (462-527)
  • _handle_number_type (530-604)
  • _handle_boolean_type (607-627)
  • _handle_array_type (204-271)
  • _handle_object_type (156-201)
mcp_fuzzer/fuzz_engine/strategy/realistic/tool_strategy.py (1)
mcp_fuzzer/fuzz_engine/strategy/schema_parser.py (1)
  • make_fuzz_strategy_from_jsonschema (52-128)
tests/unit/fuzz_engine/strategy/test_realistic_strategies.py (2)
mcp_fuzzer/fuzz_engine/strategy/realistic/tool_strategy.py (5)
  • base64_strings (19-42)
  • timestamp_strings (78-102)
  • uuid_strings (45-75)
  • generate_realistic_text (105-137)
  • fuzz_tool_arguments_realistic (140-243)
mcp_fuzzer/fuzz_engine/strategy/realistic/protocol_type_strategy.py (3)
  • json_rpc_id_values (42-56)
  • method_names (59-96)
  • protocol_version_strings (16-39)
mcp_fuzzer/fuzz_engine/strategy/aggressive/tool_strategy.py (1)
mcp_fuzzer/fuzz_engine/strategy/schema_parser.py (1)
  • make_fuzz_strategy_from_jsonschema (52-128)
tests/test_invariants.py (1)
mcp_fuzzer/fuzz_engine/invariants.py (7)
  • InvariantViolation (41-47)
  • check_response_validity (50-126)
  • check_error_type_correctness (129-185)
  • check_response_schema_conformity (188-212)
  • verify_response_invariants (215-245)
  • verify_batch_responses (248-302)
  • check_state_consistency (305-345)
mcp_fuzzer/fuzz_engine/fuzzer/protocol_fuzzer.py (1)
mcp_fuzzer/fuzz_engine/invariants.py (3)
  • verify_response_invariants (215-245)
  • InvariantViolation (41-47)
  • verify_batch_responses (248-302)
🪛 LanguageTool
docs/architecture.md

[grammar] ~247-~247: There might be a mistake here.
Context: ...d invariants and checks for fuzz testing - executor.py: Provides asynchronous execution framew...

(QB_NEW_EN)


[grammar] ~279-~279: There might be a mistake here.
Context: ...r, integer, boolean, array, object, null - String constraints: minLength, maxLength...

(QB_NEW_EN)


[grammar] ~280-~280: There might be a mistake here.
Context: ...s: minLength, maxLength, pattern, format - Number/Integer constraints: minimum, max...

(QB_NEW_EN)


[grammar] ~281-~281: There might be a mistake here.
Context: ...iveMinimum, exclusiveMaximum, multipleOf - Array constraints: minItems, maxItems, u...

(QB_NEW_EN)


[grammar] ~282-~282: There might be a mistake here.
Context: ...traints: minItems, maxItems, uniqueItems - Object constraints: required properties,...

(QB_NEW_EN)


[grammar] ~283-~283: There might be a mistake here.
Context: ...properties, minProperties, maxProperties - Schema combinations: oneOf, anyOf, allOf...

(QB_NEW_EN)


[grammar] ~284-~284: There might be a mistake here.
Context: ...Schema combinations: oneOf, anyOf, allOf - Enums and constants The module supports...

(QB_NEW_EN)


[grammar] ~295-~295: There might be a mistake here.
Context: ...ponses follow JSON-RPC 2.0 specification - check_error_type_correctness: Verifies error responses have correct ...

(QB_NEW_EN)


[grammar] ~296-~296: There might be a mistake here.
Context: ...sponses have correct structure and codes - check_response_schema_conformity: Validates responses against JSON schem...

(QB_NEW_EN)


[grammar] ~297-~297: There might be a mistake here.
Context: ...esponses against JSON schema definitions - verify_response_invariants: Orchestrates multiple invariant checks...

(QB_NEW_EN)


[grammar] ~298-~298: There might be a mistake here.
Context: ...le invariant checks on a single response - verify_batch_responses: Applies invariant checks to batches of...

(QB_NEW_EN)


[grammar] ~299-~299: There might be a mistake here.
Context: ...invariant checks to batches of responses - check_state_consistency: Ensures server state remains consisten...

(QB_NEW_EN)

⏰ 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: tests
🔇 Additional comments (7)
mcp_fuzzer/fuzz_engine/fuzzer/protocol_fuzzer.py (3)

17-21: Good addition: invariant checks are now first-class.

Importing InvariantViolation, verify_response_invariants, and verify_batch_responses brings the runtime assertions into the fuzzer path cleanly.


258-261: Results now carry invariant_violations — nice.

Attaching violations only when present avoids noise while keeping the field discoverable via the default. LGTM.


371-371: Default key in result shape is helpful for consumers.

Including invariant_violations as a defaulted field stabilizes downstream parsing.

tests/test_invariants.py (3)

172-176: LGTM: covers the no-jsonschema path with logging assertion.

Good negative-path coverage; this will prevent silent skips regressing unnoticed.


220-237: LGTM: async batch verification happy path.

Solid use of patching and asyncio marker; asserts are precise.


239-256: LGTM: async batch verification error path.

Effectively validates per-index error propagation and call counts.

tests/unit/fuzz_engine/strategy/test_realistic_strategies.py (1)

174-201: Overall: Solid, focused coverage for strategy and schema-driven paths.

Nice breadth across formats, ranges, nested shapes, and edge cases.

Copy link

github-actions bot commented Sep 4, 2025

Documentation Preview

Your documentation changes have been built successfully!

Preview URL: https://Agent-Hellboy.github.io/mcp-server-fuzzer/

The documentation will be automatically deployed to the main site when this PR is merged.


This comment was automatically generated by the documentation workflow.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (3)
tests/unit/fuzz_engine/test_executor.py (1)

206-234: Shutdown semantics mismatch: tasks aren’t cancelled on timeout.

AsyncFuzzExecutor.shutdown() only waits; if it times out, it logs a warning and returns, leaving tasks running. This test expects cancellation, which is desirable for clean teardown.

Proposed change to mcp_fuzzer/fuzz_engine/executor.py:

     async def shutdown(self, timeout: float = 5.0) -> None:
         ...
         try:
             await asyncio.wait_for(
                 asyncio.gather(*self._running_tasks, return_exceptions=True),
                 timeout=timeout,
             )
         except asyncio.TimeoutError:
             self._logger.warning(
                 "Shutdown timed out with %d tasks still running",
                 len(self._running_tasks),
             )
+            # Proactively cancel outstanding tasks and wait for them to finish
+            for task in list(self._running_tasks):
+                task.cancel()
+            await asyncio.gather(*self._running_tasks, return_exceptions=True)
+        finally:
+            # Ensure the set is cleaned up
+            self._running_tasks = {t for t in self._running_tasks if not t.cancelled() and not t.done()}

If you don’t want to change production code, relax the test to assert that shutdown returns and that tasks are either done or still pending (but that’s a weaker guarantee).

mcp_fuzzer/transport/sse.py (1)

26-40: Missing safety policy enforcement and header sanitization on network calls.

Unlike HTTPTransport, SSETransport skips is_host_allowed and sanitize_headers; this can bypass network safety policy and leak denied headers.

Apply:

+from ..safety_system.policy import is_host_allowed, sanitize_headers
@@
-        async with httpx.AsyncClient(timeout=self.timeout) as client:
-            response = await client.post(self.url, json=payload, headers=self.headers)
+        async with httpx.AsyncClient(timeout=self.timeout) as client:
+            if not is_host_allowed(self.url):
+                raise Exception("Network to non-local host is disallowed by safety policy")
+            safe_headers = sanitize_headers(self.headers)
+            response = await client.post(self.url, json=payload, headers=safe_headers)
@@
-        async with httpx.AsyncClient(timeout=self.timeout) as client:
-            response = await client.post(self.url, json=payload, headers=self.headers)
+        async with httpx.AsyncClient(timeout=self.timeout) as client:
+            if not is_host_allowed(self.url):
+                raise Exception("Network to non-local host is disallowed by safety policy")
+            safe_headers = sanitize_headers(self.headers)
+            response = await client.post(self.url, json=payload, headers=safe_headers)
@@
-        async with httpx.AsyncClient(timeout=self.timeout) as client:
+        async with httpx.AsyncClient(timeout=self.timeout) as client:
+            if not is_host_allowed(self.url):
+                raise Exception("Network to non-local host is disallowed by safety policy")
+            safe_headers = sanitize_headers(self.headers)
             async with client.stream(
                 "POST",
                 self.url,
                 json=payload,
-                headers=self.headers,
+                headers=safe_headers,
             ) as response:

Also applies to: 52-56, 66-73

mcp_fuzzer/transport/http.py (1)

126-129: Bug: _update_activity is async but not awaited.

This silently drops the coroutine and skips activity updates.

-        self._update_activity()
+        await self._update_activity()
♻️ Duplicate comments (1)
tests/unit/auth/test_auth.py (1)

27-27: Module-level pytest markers now correctly applied (pytest-style tests).

Good move switching from unittest.TestCase to pytest functions; module-level pytestmark will work with -m selection as intended.

🧹 Nitpick comments (38)
tests/unit/safety_system/test_safety.py (10)

1-1: Remove unnecessary shebang in a test module.

The file isn’t executed directly.

-#!/usr/bin/env python3

7-7: Drop unused import.

MagicMock isn’t used.

-from unittest.mock import patch, MagicMock
+from unittest.mock import patch

50-57: Make URL detection test deterministic and remove noisy prints.

Domain-only "example.com" may or may not match depending on default patterns; prefer an explicit scheme. Also remove print in tests.

         "ftp://dangerous.net",
         "file:///etc/passwd",
         "www.evil.com",
-        "example.com",
+        "https://example.com",
     ]
 
     for url in dangerous_urls:
         result = safety_filter.contains_dangerous_url(url)
-        if not result:
-            print(f"URL not detected as dangerous: {url}")
         assert result, f"URL should be detected as dangerous: {url}"

Also applies to: 61-63


98-103: Remove prints in command-pattern test.

Assertions are sufficient; prints add noise on failure.

     for command in dangerous_commands:
         result = safety_filter.contains_dangerous_command(command)
-        if not result:
-            print(f"Command not detected as dangerous: {command}")
         assert result, f"Command should be detected as dangerous: {command}"

143-144: Prefer public API over private helper in tests.

Using _sanitize_value ties tests to internals. Exercise behavior via sanitize_tool_arguments for stability.

-    result = safety_filter._sanitize_value("root", complex_value)
+    result = safety_filter.sanitize_tool_arguments("test_tool", complex_value)

105-117: Reduce reliance on private methods across tests.

Calls to _sanitize_string_argument couple tests to internals. Consider validating via sanitize_tool_arguments("tool", {"key": value}) instead. Keep one targeted unit test for the private if truly needed.

Also applies to: 120-131, 330-345, 347-359, 479-491


372-374: Fix misleading comment about recursion.

should_skip_tool_call does not recurse into nested dicts (by design); this case should not block.

-    # This should be checked because we recurse through nested structures
+    # This should NOT block: implementation checks only top-level strings and lists of strings.

499-499: Trim depth to keep unit tests fast (or mark as slow).

Depth 100 is okay but unnecessary for coverage; 30–50 keeps signal high and runtime low.

-    for i in range(100):
+    for i in range(50):

270-282: Assert a more specific logging call if feasible.

Optionally assert the first warning equals "Tool: test_tool" to make the test crisper and decouple from call_args serialization.


434-439: Add summary coverage (optional).

Consider tests for get_blocked_operations_summary() to validate counts by tool, reason, and content types after a few blocked operations.

tests/unit/fuzz_engine/strategy/test_schema_parser.py (7)

6-7: Drop unused imports.

json and the typing imports aren’t used; remove to keep tests clean.

Apply:

-import json
-from typing import Any, Dict, List

11-20: Avoid coupling tests to private helpers.

Importing and testing underscored functions ties tests to internals and increases maintenance. Prefer driving behavior via make_fuzz_strategy_from_jsonschema with schema fixtures that exercise those paths.


120-135: Strengthen format assertions.

Email/URI checks are minimal. Consider validating with email.utils.parseaddr or urllib.parse.urlparse to reduce false positives.


174-191: MaxProperties assertion may be misleading.

Generator doesn’t enforce maxProperties, but this test still passes only because defined props ≤ max. If enforcement is added later (or extra props appear), this can become brittle. Either:

  • Assert only minProperties, or
  • Add explicit enforcement in the generator and then assert it.

279-372: Great end-to-end complex schema test.

Optional: assert UUID validity with uuid.UUID(result["id"]) and validate date-time using datetime.fromisoformat try/except.


24-24: Optional: seed RNG to reduce flakiness.

If you’ve seen intermittent failures, seed random per-test.

Apply:

+import random
+
+@pytest.fixture(autouse=True)
+def _seed_rng():
+    random.seed(0)
+    yield
+    random.seed(0)

372-372: Add constraint-focused cases (multipleOf, exclusives).

These ensure numeric constraints are honored.

Apply:

+def test_integer_multiple_of_realistic():
+    schema = {"type": "integer", "minimum": 0, "maximum": 100, "multipleOf": 7}
+    v = make_fuzz_strategy_from_jsonschema(schema, phase="realistic")
+    assert isinstance(v, int)
+    assert 0 <= v <= 100
+    assert v % 7 == 0
+
+def test_number_multiple_of_realistic():
+    schema = {"type": "number", "minimum": 0.0, "maximum": 10.0, "multipleOf": 2.5}
+    v = make_fuzz_strategy_from_jsonschema(schema, phase="realistic")
+    assert isinstance(v, (int, float))
+    assert 0.0 <= v <= 10.0
+    # Allow small FP error
+    assert abs((v / 2.5) - round(v / 2.5)) < 1e-7
+
+def test_integer_exclusive_bounds():
+    schema = {"type": "integer", "minimum": 0, "exclusiveMinimum": True, "maximum": 3, "exclusiveMaximum": True}
+    v = make_fuzz_strategy_from_jsonschema(schema, phase="realistic")
+    assert v in (1, 2)
tests/unit/fuzz_engine/test_executor.py (3)

21-37: Unnecessary asyncio mark.

test_init is synchronous; dropping @pytest.mark.asyncio will shave event-loop setup overhead.

Apply:

-@pytest.mark.asyncio
-async def test_init():
+def test_init():

30-35: Avoid relying on private semaphore internals.

_semaphore._value is private and version-dependent. Prefer asserting configured values or exercising concurrency behavior instead.

Apply:

-    assert executor._semaphore._value == 5
+    assert executor._max_concurrency == 5

173-197: Semaphore usage test doesn’t catch double-acquire in batch path.

execute_batch acquires a semaphore before calling execute, which also acquires it—halving effective concurrency. Add a batch-focused test that asserts a single acquire per op, or remove the outer acquire in execute_batch.

Proposed fix in executor (outside this file):

 async def execute_batch(...):
-    async def _bounded_execute_and_track(op, args, kwargs):
-        async with self._semaphore:
-            return await self._execute_and_track(op, args, kwargs)
+    async def _bounded_execute_and_track(op, args, kwargs):
+        # execute() already enforces the semaphore
+        return await self._execute_and_track(op, args, kwargs)
tests/unit/fuzz_engine/test_invariants.py (3)

176-194: Caplog may miss the warning without level set.

Ensure the module logger’s WARNING is captured.

Apply:

+import logging
@@
-    with patch("mcp_fuzzer.fuzz_engine.invariants.HAVE_JSONSCHEMA", False):
+    with patch("mcp_fuzzer.fuzz_engine.invariants.HAVE_JSONSCHEMA", False):
+        caplog.set_level(logging.WARNING)
         assert check_response_schema_conformity(response, schema)

120-124: Also test bool error code rejection.

True/False must be rejected even though they are ints in Python.

Apply:

 def test_check_error_type_correctness_invalid_message_type():
@@
         check_error_type_correctness(error)
+
+def test_check_error_type_correctness_bool_code():
+    """Bool codes must be rejected (JSON-RPC requires integer, bool not allowed)."""
+    error = {"code": True, "message": "Invalid Request"}
+    with raises(InvariantViolation):
+        check_error_type_correctness(error)

310-310: Add guard-case tests for batch verifier.

Cover responses=None and non-list input.

Apply at end of file:

+@pytest.mark.asyncio
+async def test_verify_batch_responses_none_input():
+    results = await verify_batch_responses(None)
+    assert results == {}
+
+@pytest.mark.asyncio
+async def test_verify_batch_responses_non_list_input():
+    results = await verify_batch_responses({"jsonrpc": "2.0"})
+    assert 0 in results and "Expected a list of responses" in results[0]
tests/unit/auth/test_auth.py (2)

295-338: Add negative test for invalid MCP_CUSTOM_HEADERS JSON.

setup_auth_from_env silently ignores bad JSON; add a test to lock this behavior in and avoid regressions.

Example test to add:

def test_setup_auth_from_env_invalid_custom_headers_ignored():
    with patch.dict(os.environ, {"MCP_CUSTOM_HEADERS": "{not: json}"}, clear=True):
        auth_manager = setup_auth_from_env()
        assert isinstance(auth_manager, AuthManager)
        assert "custom" not in auth_manager.auth_providers

340-373: Broaden config test to include oauth/custom providers.

Extend the valid config test to cover oauth and custom providers to better exercise loaders.load_auth_config.

mcp_fuzzer/transport/base.py (1)

30-49: Declare _stream_request as an abstract method and type the streaming API.

Make the contract explicit and improve type-checking by annotating stream_request’s return type.

Apply:

-from typing import Any, Dict, Optional, List
+from typing import Any, Dict, Optional, List, AsyncIterator
@@
-    async def stream_request(self, payload: Dict[str, Any]):
+    async def stream_request(self, payload: Dict[str, Any]) -> AsyncIterator[Dict[str, Any]]:
@@
-        async for response in self._stream_request(payload):
+        async for response in self._stream_request(payload):
             yield response
+
+    @abstractmethod
+    async def _stream_request(self, payload: Dict[str, Any]) -> AsyncIterator[Dict[str, Any]]:
+        """Subclasses must implement streaming of requests."""
+        pass
tests/integration/test_client_transport.py (1)

27-34: Lightweight smoke test looks good; consider asserting streaming capability exists.

Optionally add a check that the transport exposes stream_request to reflect the new base API.

mcp_fuzzer/transport/sse.py (1)

1-6: Use typing.List for broader Python compatibility.

list[str] requires Python 3.9+. If supporting 3.8, switch to List[str].

-from typing import Any, Dict, Optional
+from typing import Any, Dict, Optional, List
@@
-        data_parts: list[str] = []
+        data_parts: List[str] = []

Also applies to: 101-116

mcp_fuzzer/transport/http.py (2)

63-70: Redirect fallback fabricates a trailing “/” when Location is missing.

This is unusual for 307/308; consider returning None instead to avoid unintended requests.

-        if not location and not self.url.endswith("/"):
-            location = self.url + "/"
-        if not location:
-            return None
+        if not location:
+            return None

197-206: Optional: annotate _stream_request return type for clarity.

Improve readability and tooling.

-    async def _stream_request(self, payload: Dict[str, Any]):
+    async def _stream_request(self, payload: Dict[str, Any]):
+        # type: (Dict[str, Any]) -> typing.AsyncIterator[Dict[str, Any]]

Or import AsyncIterator and annotate the signature directly.

mcp_fuzzer/transport/factory.py (2)

42-45: Parse stdio: URL command instead of forcing empty command

Permit “stdio:cmd” or “stdio://cmd” to pass a command through (keeps tests green while enabling utility).

-    if scheme == "stdio":
-        # Use empty command by default; tests only assert type
-        return StdioTransport("", **kwargs)
+    if scheme == "stdio":
+        # Allow stdio:cmd or stdio://cmd; default empty if none
+        cmd = (parsed.netloc + parsed.path).lstrip("/") if (parsed.netloc or parsed.path) else ""
+        return StdioTransport(cmd, **kwargs)

17-30: More actionable error on unsupported protocol

Include the supported set in the exception for easier debugging.

-        except KeyError:
-            raise ValueError(f"Unsupported protocol: {url_or_protocol}")
+        except KeyError:
+            supported = ", ".join(sorted(mapping.keys()))
+            raise ValueError(f"Unsupported protocol: {url_or_protocol!r}. Supported: {supported}")
mcp_fuzzer/transport/stdio.py (3)

24-26: request_id never advances; risk of ID collisions across multiple calls

Increment after each use to avoid ambiguous responses, even in tests.

-        self.request_id = 1
+        self.request_id = 1

Apply the increments in the send/stream helpers below.


213-231: Make test helper robust: use injectable IO, flush, and advance request_id

Avoid hard-wiring module-level sys; allow tests to set transport._sys while keeping sane defaults; flush writes; increment id.

-    async def _send_request(self, payload: Dict[str, Any]) -> Dict[str, Any]:
+    async def _send_request(self, payload: Dict[str, Any]) -> Dict[str, Any]:
         """Compatibility method for tests expecting sys-based stdio behavior.
@@
-        message = {**payload, "id": self.request_id, "jsonrpc": "2.0"}
-        # Do not append a newline here; some tests assert exact written content
-        sys.stdout.write(json.dumps(message))
-
-        line = sys.stdin.readline()
+        io = getattr(self, "_sys", sys)
+        message = {**payload, "id": self.request_id, "jsonrpc": "2.0"}
+        # Do not append newline here; tests assert exact payload
+        io.stdout.write(json.dumps(message))
+        if hasattr(io.stdout, "flush"):
+            io.stdout.flush()
+        # Advance id to avoid collisions
+        self.request_id += 1
+
+        line = io.stdin.readline()
         if inspect.isawaitable(line):
             line = await line
         if isinstance(line, bytes):
             line = line.decode()
         if not line:
             raise Exception("No response received on stdio")
         return json.loads(line)

139-171: Minor: json parsing errors lumped under generic Exception

Consider catching json.JSONDecodeError separately to improve logs and avoid resetting _initialized on transient payload issues.

-        except Exception as e:
+        except json.JSONDecodeError as e:
+            logging.error(f"Invalid JSON from stdio transport: {e}")
+            raise
+        except Exception as e:
             logging.error(f"Failed to receive message from stdio transport: {e}")
             self._initialized = False
             raise
tests/unit/transport/test_transport.py (3)

208-239: Use MagicMock for raise_for_status; keep aiter_lines contract closer to httpx

Slightly closer fidelity: raise_for_status is sync; aiter_lines is iterated without awaiting in most httpx versions.

-        # Create a proper AsyncMock for the response
-        mock_response = AsyncMock()
+        # Create a mock response; aclose remains awaited later
+        mock_response = AsyncMock()
@@
-        # Add raise_for_status method
-        mock_response.raise_for_status = AsyncMock()
+        # raise_for_status is sync in httpx
+        from unittest.mock import MagicMock as _MM
+        mock_response.raise_for_status = _MM()

471-502: Add coverage for streamablehttp and URL param preservation; test back-compat signature

These will lock in factory behavior and prevent regressions.

@@
 def test_create_transport_invalid_scheme():
     """Test create_transport with invalid URL scheme."""
     with pytest.raises(ValueError):
         create_transport("invalid://example.com")
+
+
+def test_create_transport_streamablehttp():
+    transport = create_transport("streamablehttp://example.com/api?x=1#frag")
+    assert isinstance(transport, HTTPTransport.__class__.__mro__[1]) or isinstance(transport, type(create_transport("http://x")))
+    # The factory returns StreamableHTTPTransport; import inside to avoid circulars
+    from mcp_fuzzer.transport.streamable_http import StreamableHTTPTransport
+    assert isinstance(transport, StreamableHTTPTransport)
+    assert transport.url == "http://example.com/api?x=1"
+
+
+def test_create_transport_sse_preserves_query():
+    transport = create_transport("sse://example.com/events?x=1")
+    assert isinstance(transport, SSETransport)
+    assert transport.url == "http://example.com/events?x=1"
+
+
+def test_create_transport_back_compat_protocol_endpoint():
+    # Old signature: (protocol, endpoint)
+    t = create_transport("https", "https://example.com/api")
+    assert isinstance(t, HTTPTransport)
+    assert t.url == "https://example.com/api"

492-496: Optional: Assert stdio command parsing when provided

After the factory enhancement, add:

 def test_create_transport_stdio():
     """Test create_transport with stdio URL."""
-    transport = create_transport("stdio:")
+    transport = create_transport("stdio:")
     assert isinstance(transport, StdioTransport)
+    # With a command
+    transport2 = create_transport("stdio:mycmd")
+    assert isinstance(transport2, StdioTransport)
+    assert transport2.command == "mycmd"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c11b082 and 26c2bf9.

📒 Files selected for processing (13)
  • mcp_fuzzer/transport/base.py (1 hunks)
  • mcp_fuzzer/transport/factory.py (1 hunks)
  • mcp_fuzzer/transport/http.py (1 hunks)
  • mcp_fuzzer/transport/sse.py (2 hunks)
  • mcp_fuzzer/transport/stdio.py (4 hunks)
  • tests/integration/test_client_transport.py (1 hunks)
  • tests/unit/auth/test_auth.py (1 hunks)
  • tests/unit/fuzz_engine/strategy/test_realistic_strategies.py (1 hunks)
  • tests/unit/fuzz_engine/strategy/test_schema_parser.py (1 hunks)
  • tests/unit/fuzz_engine/test_executor.py (1 hunks)
  • tests/unit/fuzz_engine/test_invariants.py (1 hunks)
  • tests/unit/safety_system/test_safety.py (1 hunks)
  • tests/unit/transport/test_transport.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/unit/fuzz_engine/strategy/test_realistic_strategies.py
🧰 Additional context used
🧬 Code graph analysis (12)
tests/unit/fuzz_engine/strategy/test_schema_parser.py (1)
mcp_fuzzer/fuzz_engine/strategy/schema_parser.py (8)
  • make_fuzz_strategy_from_jsonschema (52-128)
  • _handle_enum (131-153)
  • _handle_string_type (274-324)
  • _handle_integer_type (462-527)
  • _handle_number_type (530-604)
  • _handle_boolean_type (607-627)
  • _handle_array_type (204-271)
  • _handle_object_type (156-201)
mcp_fuzzer/transport/factory.py (4)
mcp_fuzzer/transport/http.py (1)
  • HTTPTransport (22-266)
mcp_fuzzer/transport/streamable_http.py (1)
  • StreamableHTTPTransport (41-370)
mcp_fuzzer/transport/sse.py (1)
  • SSETransport (10-129)
mcp_fuzzer/transport/stdio.py (1)
  • StdioTransport (20-424)
mcp_fuzzer/transport/http.py (4)
mcp_fuzzer/transport/sse.py (1)
  • _stream_request (57-99)
mcp_fuzzer/transport/stdio.py (2)
  • _stream_request (243-267)
  • _update_activity (44-49)
mcp_fuzzer/safety_system/policy.py (2)
  • is_host_allowed (69-112)
  • sanitize_headers (158-166)
tests/unit/transport/test_streamable_http.py (3)
  • post (78-82)
  • json (38-39)
  • aiter_lines (58-64)
tests/unit/fuzz_engine/test_invariants.py (1)
mcp_fuzzer/fuzz_engine/invariants.py (7)
  • InvariantViolation (41-47)
  • check_response_validity (50-126)
  • check_error_type_correctness (129-185)
  • check_response_schema_conformity (188-212)
  • verify_response_invariants (215-245)
  • verify_batch_responses (248-302)
  • check_state_consistency (305-345)
tests/unit/safety_system/test_safety.py (1)
mcp_fuzzer/safety_system/safety.py (8)
  • SafetyFilter (41-348)
  • is_safe_tool_call (413-414)
  • sanitize_tool_call (417-421)
  • create_safety_response (424-425)
  • contains_dangerous_url (84-92)
  • contains_dangerous_command (94-102)
  • _sanitize_string_argument (133-169)
  • _sanitize_value (113-131)
tests/unit/auth/test_auth.py (3)
mcp_fuzzer/auth/providers.py (9)
  • APIKeyAuth (16-25)
  • AuthProvider (6-13)
  • BasicAuth (28-39)
  • CustomHeaderAuth (54-62)
  • OAuthTokenAuth (42-51)
  • create_api_key_auth (65-66)
  • create_basic_auth (69-70)
  • create_custom_header_auth (77-78)
  • create_oauth_auth (73-74)
mcp_fuzzer/auth/manager.py (6)
  • AuthManager (6-35)
  • add_auth_provider (13-14)
  • map_tool_to_auth (16-17)
  • get_auth_for_tool (19-23)
  • get_auth_headers_for_tool (25-29)
  • get_auth_params_for_tool (31-35)
mcp_fuzzer/auth/loaders.py (2)
  • load_auth_config (59-107)
  • setup_auth_from_env (14-56)
tests/unit/transport/test_transport.py (6)
tests/unit/transport/test_streamable_http.py (4)
  • json (38-39)
  • aiter_lines (58-64)
  • raise_for_status (34-36)
  • raise_for_status (55-56)
mcp_fuzzer/transport/http.py (5)
  • HTTPTransport (22-266)
  • send_request (72-124)
  • send_raw (126-160)
  • send_notification (162-185)
  • _stream_request (197-252)
mcp_fuzzer/transport/sse.py (6)
  • SSETransport (10-129)
  • send_request (19-24)
  • send_raw (26-47)
  • send_notification (49-55)
  • _stream_request (57-99)
  • _parse_sse_event (102-129)
mcp_fuzzer/transport/stdio.py (6)
  • StdioTransport (20-424)
  • send_request (172-196)
  • send_raw (198-211)
  • send_notification (232-241)
  • _send_request (213-230)
  • _stream_request (243-267)
mcp_fuzzer/transport/base.py (7)
  • TransportProtocol (13-92)
  • send_request (15-18)
  • send_raw (21-22)
  • send_notification (25-28)
  • connect (30-32)
  • disconnect (34-36)
  • stream_request (38-48)
mcp_fuzzer/transport/factory.py (1)
  • create_transport (9-49)
mcp_fuzzer/transport/base.py (4)
mcp_fuzzer/transport/http.py (1)
  • _stream_request (197-252)
mcp_fuzzer/transport/sse.py (1)
  • _stream_request (57-99)
mcp_fuzzer/transport/stdio.py (1)
  • _stream_request (243-267)
tests/unit/transport/test_transport.py (3)
  • _stream_request (53-54)
  • _stream_request (87-88)
  • _stream_request (123-126)
tests/integration/test_client_transport.py (2)
mcp_fuzzer/client/protocol_client.py (1)
  • ProtocolClient (19-399)
mcp_fuzzer/transport/streamable_http.py (1)
  • StreamableHTTPTransport (41-370)
mcp_fuzzer/transport/stdio.py (4)
tests/unit/transport/test_transport.py (6)
  • _send_request (50-51)
  • _send_request (83-85)
  • _send_request (120-121)
  • _stream_request (53-54)
  • _stream_request (87-88)
  • _stream_request (123-126)
tests/unit/transport/test_streamable_http.py (1)
  • json (38-39)
mcp_fuzzer/transport/http.py (1)
  • _stream_request (197-252)
mcp_fuzzer/transport/sse.py (1)
  • _stream_request (57-99)
mcp_fuzzer/transport/sse.py (3)
mcp_fuzzer/transport/http.py (1)
  • _stream_request (197-252)
mcp_fuzzer/transport/stdio.py (1)
  • _stream_request (243-267)
tests/unit/transport/test_transport.py (3)
  • _stream_request (53-54)
  • _stream_request (87-88)
  • _stream_request (123-126)
tests/unit/fuzz_engine/test_executor.py (1)
mcp_fuzzer/fuzz_engine/executor.py (5)
  • AsyncFuzzExecutor (11-244)
  • execute (47-95)
  • execute_with_retry (97-151)
  • execute_batch (153-199)
  • _execute_and_track (201-218)
⏰ 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: tests
🔇 Additional comments (15)
tests/unit/safety_system/test_safety.py (1)

16-17: Markers look good for component-based runs.

pytestmark = [pytest.mark.unit, pytest.mark.safety_system] aligns with the PR’s component selection goals.

tests/unit/fuzz_engine/strategy/test_schema_parser.py (7)

22-22: Markers look good.

Component and unit markers align with the PR’s component-based test selection.


25-53: Solid coverage of basic types.

Assertions are tolerant to generator variability.


55-63: Array test is resilient to empty outputs.

Good guard against empty arrays.


65-83: Object generation checks are appropriate.

Verifies required fields without over-constraining optional ones.


85-91: Enum handling covered.

Realistic phase expectations are correct.


93-118: Combinations (oneOf/anyOf/allOf) covered.

Good functional checks without brittleness.


193-211: Enum aggressive case is safe here.

Aggressive invalids remain strings for string enums, so the type assertion holds.

tests/unit/fuzz_engine/test_executor.py (1)

111-151: Batch success/mixed tests look solid.

Order-insensitive checks avoid flakiness.

tests/unit/fuzz_engine/test_invariants.py (3)

241-258: Batch all-valid path is good.

Mocks align with async to_thread usage.


260-278: Batch mixed path is good.

Clear signal of per-item outcomes.


304-310: State consistency negative path covered well.

Validates unexpected change handling.

mcp_fuzzer/transport/sse.py (1)

22-24: SSETransport.send_request change is isolated—no production code invokes it.

mcp_fuzzer/transport/factory.py (1)

36-37: LGTM for http(s) passthrough

Single-URL http/https path is clear and backward compatible.

mcp_fuzzer/transport/stdio.py (1)

243-268: Refactor _stream_request to use subprocess pipes
Add a branch in _stream_request that writes to the child process’s stdin and reads from its stdout (e.g. via await self.process_manager.get_process()), falling back to the patched self._sys for tests. No production code invokes StdioTransport or streams over module sys outside of test fixtures, so this refactor is safe.

Comment on lines +39 to +43
def test_check_response_validity_valid_notification():
"""Test that a valid JSON-RPC notification passes validation."""
response = {"jsonrpc": "2.0"}
assert check_response_validity(response)

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Notification example is invalid JSON-RPC.

A notification must include a method and no id. Update the fixture.

Apply:

-response = {"jsonrpc": "2.0"}
+response = {"jsonrpc": "2.0", "method": "notify", "params": {}}
📝 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
def test_check_response_validity_valid_notification():
"""Test that a valid JSON-RPC notification passes validation."""
response = {"jsonrpc": "2.0"}
assert check_response_validity(response)
def test_check_response_validity_valid_notification():
"""Test that a valid JSON-RPC notification passes validation."""
response = {"jsonrpc": "2.0", "method": "notify", "params": {}}
assert check_response_validity(response)
🤖 Prompt for AI Agents
In tests/unit/fuzz_engine/test_invariants.py around lines 39 to 43, the test
uses an invalid JSON-RPC notification fixture that only contains "jsonrpc";
update the fixture to include a "method" field (e.g., a valid method name
string) and ensure there is no "id" field so it represents a proper
notification; keep the assertion calling check_response_validity(response)
unchanged.

Copy link

github-actions bot commented Sep 4, 2025

Documentation Preview

Your documentation changes have been built successfully!

Preview URL: https://Agent-Hellboy.github.io/mcp-server-fuzzer/

The documentation will be automatically deployed to the main site when this PR is merged.


This comment was automatically generated by the documentation workflow.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
tests/unit/fuzz_engine/fuzzer/test_tool_fuzzer.py (1)

442-456: Fix: logging test patches the module, but the code uses an instance logger.
ToolFuzzer logs via self._logger.info; patching the module won’t observe those calls, making this test flaky/incorrect.

Apply:

-        with patch("mcp_fuzzer.fuzz_engine.fuzzer.tool_fuzzer.logging") as mock_logging:
-            await self.fuzzer.fuzz_tool(tool, runs=1)
-
-            # Check that logging.info was called
-            mock_logging.info.assert_called()
-
-            # Check that the log message contains expected information
-            call_args = mock_logging.info.call_args_list
-            found_tool = False
-            for call_args_tuple in call_args:
-                if "test_tool" in str(call_args_tuple):
-                    found_tool = True
-                    break
-            self.assertTrue(found_tool)
+        with patch.object(self.fuzzer, "_logger") as mock_logger:
+            await self.fuzzer.fuzz_tool(tool, runs=1)
+            mock_logger.info.assert_called()
+            # Ensure the tool name made it into an info log
+            calls = [str(c) for c in mock_logger.info.call_args_list]
+            self.assertTrue(any("test_tool" in c for c in calls))
♻️ Duplicate comments (1)
mcp_fuzzer/fuzz_engine/strategy/schema_parser.py (1)

116-131: Const support added — thanks for addressing earlier feedback.

This implements the previously requested fast-path for "const".

🧹 Nitpick comments (14)
mcp_fuzzer/fuzz_engine/fuzzer/protocol_fuzzer.py (4)

44-45: Make batch validation timeout configurable.

Expose BATCH_VALIDATION_TIMEOUT via ctor arg or env var to tune for slower servers in CI.

-    # Seconds to wait for invariant validation of batch responses
-    BATCH_VALIDATION_TIMEOUT: ClassVar[float] = 5.0
+    # Seconds to wait for invariant validation of batch responses
+    BATCH_VALIDATION_TIMEOUT: ClassVar[float] = 5.0
+
+    def __init__(self, transport: Optional[Any] = None, max_concurrency: int = 5,
+                 batch_validation_timeout: Optional[float] = None):
+        ...
+        self.BATCH_VALIDATION_TIMEOUT = (
+            batch_validation_timeout
+            if batch_validation_timeout is not None
+            else self.BATCH_VALIDATION_TIMEOUT
+        )

222-252: Good invariant integration; consider passing context.

Pass expected error codes and/or schema when known to improve signal; also consider logging count of violations for batches.

-                            batch = await asyncio.wait_for(
-                                verify_batch_responses(server_response),
+                            batch = await asyncio.wait_for(
+                                verify_batch_responses(
+                                    server_response,
+                                    # expected_error_codes=expected_codes,
+                                    # schema=known_schema,
+                                ),
                                 timeout=self.BATCH_VALIDATION_TIMEOUT,
                             )

258-261: Reflect invariant violations in success flag.

If invariants fail, set success=False to surface failures in summaries.

-            if invariant_violations:
-                result["invariant_violations"] = invariant_violations
+            if invariant_violations:
+                result["invariant_violations"] = invariant_violations
+                result["success"] = False

436-446: Avoid hard-coded 30s per-type timeout.

Make per-type timeout configurable; cancel with shielded cleanup to avoid leaks.

-                results = await asyncio.wait_for(task, timeout=30.0)
+                results = await asyncio.wait_for(task, timeout=self._per_type_timeout)

(Add _per_type_timeout as a ctor arg with a sane default.)

mcp_fuzzer/fuzz_engine/strategy/schema_parser.py (4)

28-31: Expose recursion depth tuning and guard unsatisfied schemas.

Make MAX_RECURSION_DEPTH configurable and, when exceeded, prefer schema-compliant fallbacks (e.g., respect type lists fully) to reduce invalid outputs in “realistic”.

-MAX_RECURSION_DEPTH = 5
+MAX_RECURSION_DEPTH = int(os.getenv("MCP_MAX_SCHEMA_DEPTH", "5"))

(Import os.)


235-313: uniqueItems may still emit duplicates after 10 attempts.

Switch to a while-based fill loop that enforces uniqueness deterministically in “realistic”; relax only in “aggressive”.

-    result = []
-    seen_values = set()  # For uniqueItems constraint
-
-    for _ in range(array_size):
+    result: List[Any] = []
+    seen_values: set[str] = set()  # For uniqueItems constraint
+    def _stable_hash(v) -> str:
+        import json as _json
+        try: return _json.dumps(v, sort_keys=True, default=str)
+        except Exception: return repr(v)
+    while len(result) < array_size:
         # Generate item based on items schema
         item = make_fuzz_strategy_from_jsonschema(
             items_schema, phase, recursion_depth + 1
         )
-
-        # Handle uniqueItems constraint
-        if unique_items:
-            # For simple types, ensure uniqueness
-            import json as _json
-
-            attempts = 0
-            while attempts < 10:
-                try:
-                    item_hash = _json.dumps(item, sort_keys=True, default=str)
-                except Exception:
-                    # Fallback to repr if dumps fails
-                    item_hash = repr(item)
-                if item_hash not in seen_values:
-                    seen_values.add(item_hash)
-                    break
-                item = make_fuzz_strategy_from_jsonschema(
-                    items_schema, phase, recursion_depth + 1
-                )
-                attempts += 1
-
-        result.append(item)
+        if unique_items:
+            item_hash = _stable_hash(item)
+            if item_hash in seen_values:
+                if phase == "realistic":
+                    continue  # try again to maintain uniqueness
+                # aggressive: allow occasional dup
+            else:
+                seen_values.add(item_hash)
+        result.append(item)

315-366: Regex generation is very limited; consider optional lib integration.

Optionally use rstr/exrex when available to cover more patterns; fallback to current logic.

-            return _generate_string_from_pattern(pattern, min_length, max_length)
+            try:
+                import rstr  # type: ignore
+                return rstr.xeger(pattern)[:max_length]
+            except Exception:
+                return _generate_string_from_pattern(pattern, min_length, max_length)

571-646: Float multipleOf: reduce FP error with Decimal quantization.

Floating rounding can miss valid multiples; prefer Decimal when multipleOf is finite.

-        if multiple_of:
+        if multiple_of and math.isfinite(float(multiple_of)):
             try:
-                m = float(multiple_of)
+                from decimal import Decimal, getcontext
+                getcontext().prec = 28
+                m = Decimal(str(multiple_of))
+                dmin, dmax = Decimal(str(minimum)), Decimal(str(maximum))
@@
-                    k_start = math.ceil(minimum / m)
-                    k_end = math.floor(maximum / m)
+                    k_start = math.ceil((dmin / m))
+                    k_end = math.floor((dmax / m))
@@
-                        value = k * m
+                        value = float(Decimal(k) * m)
tests/unit/fuzz_engine/fuzzer/test_tool_fuzzer.py (6)

131-136: Tighten mock to real signature with autospec for coroutine.
Use autospec with AsyncMock so calls that drift from the real async signature fail loudly.

Apply:

-        with patch.object(
-            self.fuzzer, "fuzz_tool", new_callable=AsyncMock
-        ) as mock_fuzz:
+        with patch.object(
+            self.fuzzer, "fuzz_tool", new_callable=AsyncMock, autospec=True
+        ) as mock_fuzz:

164-166: Cover the negative safety path.
You only test the “safe” branch. Add a test asserting safety_blocked=True and safety_reason when is_safe_tool_call returns False.

Here’s a concise test you can drop in:

@pytest.mark.asyncio
@patch("mcp_fuzzer.fuzz_engine.fuzzer.tool_fuzzer.is_safe_tool_call", return_value=False)
async def test_fuzz_tool_safety_blocked(mock_is_safe):
    fuzzer = ToolFuzzer()
    tool = {"name": "danger_tool", "inputSchema": {"properties": {"p": {"type": "string"}}}}
    res = await fuzzer.fuzz_tool(tool, runs=1)
    assert len(res) == 1
    assert res[0]["success"] is False
    assert res[0].get("safety_blocked") is True
    assert "safety_reason" in res[0]

Also applies to: 304-306


185-194: Assert sanitize was used with expected inputs.
Lock in behavior by verifying sanitize_tool_call was called once with "complex_tool" and the pre-sanitized args.

Add after the await:

mock_sanitize.assert_called_once()
called_tool, called_args = mock_sanitize.call_args[0]
self.assertEqual(called_tool, "complex_tool")
self.assertIsInstance(called_args, dict)

322-325: Avoid magic numbers in side_effect; derive from a single constant.
Prevents future drift between the comprehension and the runs argument.

Apply:

-        mock_strategies.fuzz_tool_arguments.side_effect = [
-            {"name": f"test_{i}", "count": i, "enabled": i % 2 == 0} for i in range(5)
-        ]
+        num_runs = 5
+        mock_strategies.fuzz_tool_arguments.side_effect = [
+            {"name": f"test_{i}", "count": i, "enabled": i % 2 == 0}
+            for i in range(num_runs)
+        ]

And update the call to use num_runs:

-        results = await self.fuzzer.fuzz_tool(tool, runs=5)
+        results = await self.fuzzer.fuzz_tool(tool, runs=num_runs)

424-431: Clarify expectation for None tool name.
Current assertion accepts both "unknown" and None. Given implementation returns the original name (even if None), make the expectation deterministic (None) or split into two tests (sanitized vs original).

For a deterministic check now:

-            self.assertIn(result["tool_name"], ["unknown", None])
+            self.assertIsNone(result["tool_name"])

If behavior changes to prefer sanitized names later, update accordingly.


469-474: Phase assertion could be parameterized for both realistic and aggressive.
Covers strategy routing logic across phases in one place.

Example:

@pytest.mark.asyncio
@pytest.mark.parametrize("phase", ["aggressive", "realistic"])
async def test_strategy_integration_param(self, phase):
    tool = {"name": "test_tool", "inputSchema": {"properties": {"name": {"type": "string"}}}}
    with patch.object(self.fuzzer.strategies, "fuzz_tool_arguments") as mock_fuzz:
        mock_fuzz.return_value = {"name": "v"}
        await self.fuzzer.fuzz_tool(tool, runs=1, phase=phase)
        mock_fuzz.assert_called_with(tool, phase=phase)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 26c2bf9 and 6dec921.

📒 Files selected for processing (6)
  • mcp_fuzzer/fuzz_engine/fuzzer/protocol_fuzzer.py (6 hunks)
  • mcp_fuzzer/fuzz_engine/invariants.py (1 hunks)
  • mcp_fuzzer/fuzz_engine/strategy/schema_parser.py (1 hunks)
  • tests/add_markers.py (1 hunks)
  • tests/integration/test_client_transport.py (1 hunks)
  • tests/unit/fuzz_engine/fuzzer/test_tool_fuzzer.py (12 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/add_markers.py
  • mcp_fuzzer/fuzz_engine/invariants.py
  • tests/integration/test_client_transport.py
🧰 Additional context used
🧬 Code graph analysis (2)
mcp_fuzzer/fuzz_engine/fuzzer/protocol_fuzzer.py (2)
mcp_fuzzer/types.py (1)
  • FuzzDataResult (12-22)
mcp_fuzzer/fuzz_engine/invariants.py (3)
  • verify_response_invariants (222-252)
  • InvariantViolation (41-47)
  • verify_batch_responses (255-309)
tests/unit/fuzz_engine/fuzzer/test_tool_fuzzer.py (2)
mcp_fuzzer/fuzz_engine/fuzzer/tool_fuzzer.py (1)
  • ToolFuzzer (21-257)
mcp_fuzzer/fuzz_engine/strategy/strategy_manager.py (1)
  • fuzz_tool_arguments (54-61)
⏰ 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: component-tests
  • GitHub Check: tests
🔇 Additional comments (6)
mcp_fuzzer/fuzz_engine/fuzzer/protocol_fuzzer.py (1)

311-312: Apply TypedDict updates to FuzzDataResult
Update mcp_fuzzer/types.py so that server_response allows List[Any] and add the new invariant_violations field—no dict-only access patterns were found, so this change is safe.

--- a/mcp_fuzzer/types.py
+++ b/mcp_fuzzer/types.py
@@ class FuzzDataResult(TypedDict, total=False):
-    server_response: Optional[Dict[str, Any]]
+    server_response: Optional[Union[Dict[str, Any], List[Any]]]
@@ class FuzzDataResult(TypedDict, total=False):
     exception: Optional[str]
+    invariant_violations: List[str]
tests/unit/fuzz_engine/fuzzer/test_tool_fuzzer.py (5)

9-9: Good addition: AsyncMock import aligns tests with async API.
This enables correct patching of coroutine functions used by the fuzzer.


195-203: LGTM: deterministic mocks keep complex-schema test stable.
The sanitize and strategy mocks make this test resilient while still validating integration points.


241-251: LGTM: empty schema case is exercised without over-constraining behavior.
Asserting dict structure only is appropriate here.


327-327: Re-instantiating fuzzer to pick up patched ToolStrategies is correct.
This ensures the constructor uses the mocked factory.


528-529: LGTM: awaiting both fuzzer instances enforces true async execution and isolation.
This matches the async API and avoids false positives.

Comment on lines 32 to 60
def _merge_allOf(schemas: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Deep merge for allOf schemas."""
merged: Dict[str, Any] = {}
props: Dict[str, Any] = {}
required: List[str] = []
merged_types = None # track intersection of declared types
for s in schemas:
if "properties" in s and isinstance(s["properties"], dict):
props.update(s["properties"])
if "required" in s and isinstance(s["required"], list):
required.extend([r for r in s["required"] if isinstance(r, str)])
t = s.get("type")
if t is not None:
tset = set(t if isinstance(t, list) else [t])
merged_types = tset if merged_types is None else (merged_types & tset)
for k, v in s.items():
if k not in ("properties", "required"):
merged[k] = v if k not in merged else merged[k]
if props:
merged["properties"] = props
if required:
merged["required"] = sorted(set(required))
if merged_types:
if len(merged_types) > 1:
merged["type"] = list(merged_types)
else:
merged["type"] = next(iter(merged_types))
return merged

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

allOf merge is too lossy for constraints; combine restrictively.

Current merge keeps first/last values arbitrarily, producing invalid “realistic” data when constraints conflict. Merge numerics/lengths using max(minimum/minLength) and min(maximum/maxLength); combine required; intersect types.

 def _merge_allOf(schemas: List[Dict[str, Any]]) -> Dict[str, Any]:
@@
-    merged: Dict[str, Any] = {}
+    merged: Dict[str, Any] = {}
     props: Dict[str, Any] = {}
     required: List[str] = []
     merged_types = None  # track intersection of declared types
+    mins = {}; maxs = {};  # name -> list
+    for k in ("minimum","minLength","exclusiveMinimum"):
+        mins[k]=[]
+    for k in ("maximum","maxLength","exclusiveMaximum"):
+        maxs[k]=[]
     for s in schemas:
@@
-        for k, v in s.items():
-            if k not in ("properties", "required"):
-                merged[k] = v if k not in merged else merged[k]
+        for k, v in s.items():
+            if k in mins and isinstance(v, (int, float)):
+                mins[k].append(v)
+            elif k in maxs and isinstance(v, (int, float)):
+                maxs[k].append(v)
+            elif k not in ("properties", "required", "type"):
+                merged.setdefault(k, v)
@@
     if merged_types:
@@
     return merged
+    # After loop:
+    for k, vals in mins.items():
+        if vals: merged[k] = max(vals)
+    for k, vals in maxs.items():
+        if vals: merged[k] = min(vals)

(Adjust for boolean-style exclusives as needed.)

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In mcp_fuzzer/fuzz_engine/strategy/schema_parser.py around lines 32-60, the
allOf merge currently preserves first/last values and loses constraints,
producing invalid merged schemas; change the merge to be restrictive: for
numeric bounds (minimum/maximum) and length bounds (minLength/maxLength) compute
the most restrictive values (maximum of all minima, minimum of all maxima) and
if exclusivity flags (exclusiveMinimum/exclusiveMaximum) are present treat them
as restrictive (e.g., if any schema sets exclusive true for the active boundary,
mark it exclusive); intersect types as already intended but fail/return a
clearly impossible schema when intersections are empty; intersect enums (take
set intersection) and for boolean-style exclusives or other constraint pairs
choose the stricter interpretation (e.g., exclusive beats inclusive when equal);
keep combining required and properties as before but replace simple overwrite
behavior for other constraint keys by applying the more restrictive value
instead (prefer intersection/stricter numeric/length constraint where
applicable).

Comment on lines 187 to 233
def _handle_object_type(
schema: Dict[str, Any], phase: str, recursion_depth: int
) -> Dict[str, Any]:
"""Handle object type schema."""
result = {}

# Get properties
properties = schema.get("properties", {})
required = schema.get("required", [])

# Handle property constraints
min_properties = schema.get("minProperties", 0)
# We don't need to explicitly use max_properties as we're not
# enforcing a maximum number of properties in the generated objects

# Process each property
for prop_name, prop_schema in properties.items():
# For required properties or by chance for optional ones
if prop_name in required or random.random() < 0.8:
result[prop_name] = make_fuzz_strategy_from_jsonschema(
prop_schema, phase, recursion_depth + 1
)

# Ensure we meet minProperties constraint
if len(result) < min_properties:
# Add additional properties if needed
additional_count = min_properties - len(result)
for i in range(additional_count):
prop_name = f"additional_prop_{i}"
result[prop_name] = _generate_default_value(phase)

# In aggressive mode, sometimes add extra properties
if phase == "aggressive" and random.random() < 0.3:
# Add some potentially problematic properties
extra_props = {
"__proto__": {"isAdmin": True},
"constructor": {"prototype": {"isAdmin": True}},
"eval": "console.log('code injection')",
}
result.update(
random.sample(
extra_props.items(), k=min(len(extra_props), random.randint(1, 3))
)
)

return result

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Honor additionalProperties when adding or padding properties.

Respect additionalProperties: false; don’t synthesize additional_prop_* or extra props in that case. Also, prefer patternProperties when available to satisfy minProperties.

-    # Ensure we meet minProperties constraint
+    # Ensure we meet minProperties constraint
     if len(result) < min_properties:
-        # Add additional properties if needed
-        additional_count = min_properties - len(result)
-        for i in range(additional_count):
-            prop_name = f"additional_prop_{i}"
-            result[prop_name] = _generate_default_value(phase)
+        allow_additional = schema.get("additionalProperties", True) is not False
+        if allow_additional:
+            additional_count = min_properties - len(result)
+            for i in range(additional_count):
+                prop_name = f"additional_prop_{i}"
+                result[prop_name] = _generate_default_value(phase)
+        # If not allowed, leave as-is; schema may be unsatisfiable w.r.t minProperties.
@@
-    if phase == "aggressive" and random.random() < 0.3:
+    if phase == "aggressive" and random.random() < 0.3 and schema.get("additionalProperties", True) is not False:
📝 Committable suggestion

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

Suggested change
def _handle_object_type(
schema: Dict[str, Any], phase: str, recursion_depth: int
) -> Dict[str, Any]:
"""Handle object type schema."""
result = {}
# Get properties
properties = schema.get("properties", {})
required = schema.get("required", [])
# Handle property constraints
min_properties = schema.get("minProperties", 0)
# We don't need to explicitly use max_properties as we're not
# enforcing a maximum number of properties in the generated objects
# Process each property
for prop_name, prop_schema in properties.items():
# For required properties or by chance for optional ones
if prop_name in required or random.random() < 0.8:
result[prop_name] = make_fuzz_strategy_from_jsonschema(
prop_schema, phase, recursion_depth + 1
)
# Ensure we meet minProperties constraint
if len(result) < min_properties:
# Add additional properties if needed
additional_count = min_properties - len(result)
for i in range(additional_count):
prop_name = f"additional_prop_{i}"
result[prop_name] = _generate_default_value(phase)
# In aggressive mode, sometimes add extra properties
if phase == "aggressive" and random.random() < 0.3:
# Add some potentially problematic properties
extra_props = {
"__proto__": {"isAdmin": True},
"constructor": {"prototype": {"isAdmin": True}},
"eval": "console.log('code injection')",
}
result.update(
random.sample(
extra_props.items(), k=min(len(extra_props), random.randint(1, 3))
)
)
return result
# Ensure we meet minProperties constraint
if len(result) < min_properties:
allow_additional = schema.get("additionalProperties", True) is not False
if allow_additional:
additional_count = min_properties - len(result)
for i in range(additional_count):
prop_name = f"additional_prop_{i}"
result[prop_name] = _generate_default_value(phase)
# If not allowed, leave as-is; schema may be unsatisfiable w.r.t minProperties.
# In aggressive mode, sometimes add extra properties
if phase == "aggressive" and random.random() < 0.3 and schema.get("additionalProperties", True) is not False:
# Add some potentially problematic properties
extra_props = {
"__proto__": {"isAdmin": True},
"constructor": {"prototype": {"isAdmin": True}},
"eval": "console.log('code injection')",
}
result.update(
random.sample(
extra_props.items(), k=min(len(extra_props), random.randint(1, 3))
)
)
🤖 Prompt for AI Agents
In mcp_fuzzer/fuzz_engine/strategy/schema_parser.py around lines 187 to 233, the
code currently always synthesizes additional properties and aggressive extra
props without honoring additionalProperties or patternProperties; update the
logic so that when adding/padding properties you first check
schema.get("additionalProperties", True) and do not create any additional_prop_*
or extra_props if that value is False; if patternProperties exists, prefer to
satisfy minProperties by creating entries whose names match one of the
patternProperties keys and use make_fuzz_strategy_from_jsonschema with the
corresponding patternProperties schema (or a merged schema with
additionalProperties when present); when falling back to generated names only do
so if additionalProperties is True; likewise guard the aggressive-mode injection
of prototype/constructor/eval props by additionalProperties and avoid injecting
reserved keys when disallowed.

from unittest.mock import MagicMock, call, patch, AsyncMock
from mcp_fuzzer.fuzz_engine.fuzzer.tool_fuzzer import ToolFuzzer

pytestmark = [pytest.mark.unit, pytest.mark.fuzz_engine, pytest.mark.fuzzer]
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Project-wide markers: register them to avoid PytestUnknownMarkWarning and to support component-scoped runs.
Nice touch adding unit/fuzz_engine/fuzzer markers; make sure they’re declared in pytest config so CI can select changed components per #70.

Run this to verify marker registration:

If missing, add in pytest.ini or pyproject.toml (tool.pytest.ini_options.markers):

  • unit
  • fuzz_engine
  • fuzzer
    Then CI can run: pytest -m "unit and fuzz_engine and fuzzer".

🏁 Script executed:

#!/bin/bash
# Look for marker declarations in pytest config files
fd -a 'pyproject.toml|pytest.ini|tox.ini' -x rg -nC1 -i 'markers|ini_options'

Length of output: 306


Add the unit, fuzz_engine, and fuzzer markers to your pytest config: the project’s [tool.pytest.ini_options] currently only declares "auth"; you must append entries for unit, fuzz_engine, and fuzzer under markers in pyproject.toml or pytest.ini so that pytest -m "unit and fuzz_engine and fuzzer" runs without warnings.

🤖 Prompt for AI Agents
In tests/unit/fuzz_engine/fuzzer/test_tool_fuzzer.py around line 12, the test
file uses pytest markers "unit", "fuzz_engine", and "fuzzer" but the project
pytest config only declares "auth", causing marker warnings; update
pyproject.toml (or pytest.ini) under [tool.pytest.ini_options] -> markers (or
markers list in pytest.ini) to include "unit", "fuzz_engine", and "fuzzer"
entries so pytest recognizes them (add each marker as a separate string entry or
line).

Copy link

github-actions bot commented Sep 4, 2025

Documentation Preview

Your documentation changes have been built successfully!

Preview URL: https://Agent-Hellboy.github.io/mcp-server-fuzzer/

The documentation will be automatically deployed to the main site when this PR is merged.


This comment was automatically generated by the documentation workflow.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
mcp_fuzzer/fuzz_engine/strategy/aggressive/tool_strategy.py (1)

245-253: Schema parser fallback added — good parity with realistic.

The try/except around make_fuzz_strategy_from_jsonschema prevents hard crashes and aligns behavior with the realistic strategy. Nice.

🧹 Nitpick comments (4)
mcp_fuzzer/fuzz_engine/strategy/aggressive/tool_strategy.py (2)

264-289: Handle multi-type schemas (type as list).

Property type may be a list per JSON Schema. Randomly choose one to diversify aggressive inputs.

-            if random.random() < 0.8:  # 80% chance to include each property
-                prop_type = prop_spec.get("type", "string")
+            if random.random() < 0.8:  # 80% chance to include each property
+                prop_type = prop_spec.get("type", "string")
+                if isinstance(prop_type, list) and prop_type:
+                    prop_type = random.choice(prop_type)

291-295: Optionally make required fields structurally valid for objects/arrays.

Even in aggressive mode, using {}/[] for required object/array fields can keep servers from short-circuiting on type errors, allowing deeper fuzzing.

-    for key in required or []:
-        if key not in args:
-            args[key] = generate_aggressive_text()
+    for key in required or []:
+        if key not in args:
+            spec = (schema.get("properties") or {}).get(key, {}) if isinstance(schema, dict) else {}
+            t = spec.get("type")
+            if t == "object":
+                args[key] = {}
+            elif t == "array":
+                args[key] = []
+            else:
+                args[key] = generate_aggressive_text()
mcp_fuzzer/fuzz_engine/strategy/realistic/tool_strategy.py (2)

171-207: Drop inner re-import; reuse the already-imported parser.

The extra import inside the loop adds overhead and noise. Call the function you imported at Line 142.

-                        for _ in range(count):
-                            try:
-                                from ..schema_parser import (
-                                    make_fuzz_strategy_from_jsonschema as _mk,
-                                )
-
-                                val = _mk(items_schema, phase="realistic")
+                        for _ in range(count):
+                            try:
+                                val = make_fuzz_strategy_from_jsonschema(
+                                    items_schema, phase="realistic"
+                                )

208-220: Same here: remove inner re-import for nested objects.

-                    try:
-                        from ..schema_parser import (
-                            make_fuzz_strategy_from_jsonschema as _mk,
-                        )
-
-                        nested = _mk(prop_spec, phase="realistic")
+                    try:
+                        nested = make_fuzz_strategy_from_jsonschema(
+                            prop_spec, phase="realistic"
+                        )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6dec921 and f7b4ff2.

📒 Files selected for processing (2)
  • mcp_fuzzer/fuzz_engine/strategy/aggressive/tool_strategy.py (1 hunks)
  • mcp_fuzzer/fuzz_engine/strategy/realistic/tool_strategy.py (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-08T11:50:01.877Z
Learnt from: Agent-Hellboy
PR: Agent-Hellboy/mcp-server-fuzzer#25
File: mcp_fuzzer/strategy/tool_strategies.py:105-139
Timestamp: 2025-08-08T11:50:01.877Z
Learning: Repo Agent-Hellboy/mcp-server-fuzzer: In mcp_fuzzer/strategy/tool_strategies.py, ToolStrategies._generate_aggressive_integer can return None in the "special" branch despite an int annotation. Maintainership decision: ignore for now and handle in issue #18; do not re-flag this as blocking in interim reviews.

Applied to files:

  • mcp_fuzzer/fuzz_engine/strategy/aggressive/tool_strategy.py
🧬 Code graph analysis (2)
mcp_fuzzer/fuzz_engine/strategy/aggressive/tool_strategy.py (1)
mcp_fuzzer/fuzz_engine/strategy/schema_parser.py (1)
  • make_fuzz_strategy_from_jsonschema (62-159)
mcp_fuzzer/fuzz_engine/strategy/realistic/tool_strategy.py (1)
mcp_fuzzer/fuzz_engine/strategy/schema_parser.py (1)
  • make_fuzz_strategy_from_jsonschema (62-159)
⏰ 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: tests
  • GitHub Check: component-tests
🔇 Additional comments (2)
mcp_fuzzer/fuzz_engine/strategy/realistic/tool_strategy.py (2)

142-157: Schema-driven generation with guards — LGTM.

Local import, non-dict schema guard, and parser try/except produce a robust baseline.


16-16: No action needed: Hypothesis is declared
Hypothesis appears in pyproject.toml (line 33).

Comment on lines 247 to 261
schema = tool.get("inputSchema", {})
properties = schema.get("properties", {})

args = {}
# Use the enhanced schema parser to generate aggressive values
try:
args = make_fuzz_strategy_from_jsonschema(schema, phase="aggressive")
except Exception:
args = {}

for prop_name, prop_spec in properties.items():
prop_type = prop_spec.get("type", "string")
# If the schema parser returned something other than a dict, create a default dict
if not isinstance(args, dict):
args = {}

# Generate aggressive values based on type
if prop_type == "string":
args[prop_name] = generate_aggressive_text()
elif prop_type == "integer":
args[prop_name] = _generate_aggressive_integer()
elif prop_type == "number":
args[prop_name] = _generate_aggressive_float()
elif prop_type == "boolean":
# Use non-boolean values to test validation
args[prop_name] = random.choice(
[
None,
"",
"true",
"false",
0,
1,
-1,
42,
[],
{},
"yes",
"no",
"on",
"off",
"True",
"False",
]
)
elif prop_type == "array":
# Generate malformed arrays
aggressive_arrays = [
None, # Not an array
"not_an_array", # String instead
[generate_aggressive_text() for _ in range(random.randint(0, 10))],
[None] * random.randint(1, 5), # Array of nulls
[{}] * random.randint(1, 3), # Array of empty objects
[], # Empty array
["A" * 10000], # Huge string in array
list(range(10000)), # Huge array
]
args[prop_name] = random.choice(aggressive_arrays)
elif prop_type == "object":
# Generate malformed objects
aggressive_objects = [
None, # Not an object
"not_an_object", # String instead
[], # Array instead
{}, # Empty object
{generate_aggressive_text(): generate_aggressive_text()},
{"evil": "<script>alert('xss')</script>"},
{"\x00null\x00": "null_key"},
{"a" * 1000: "huge_key"},
]
args[prop_name] = random.choice(aggressive_objects)
else:
# Fallback for unknown types - try to break them
args[prop_name] = random.choice(
[
None,
generate_aggressive_text(),
_generate_aggressive_integer(),
_generate_aggressive_float(),
[],
{},
]
)
# Ensure we have at least some arguments
if not args and schema.get("properties"):
# Fallback to basic property handling
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Guard against non-dict schemas to avoid AttributeError at .get.

If tool["inputSchema"] is not a dict, the later schema.get("properties") access will raise. Mirror the realistic strategy’s type check.

-    schema = tool.get("inputSchema", {})
+    schema = tool.get("inputSchema")
+    if not isinstance(schema, dict):
+        schema = {}
📝 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
schema = tool.get("inputSchema", {})
properties = schema.get("properties", {})
args = {}
# Use the enhanced schema parser to generate aggressive values
try:
args = make_fuzz_strategy_from_jsonschema(schema, phase="aggressive")
except Exception:
args = {}
for prop_name, prop_spec in properties.items():
prop_type = prop_spec.get("type", "string")
# If the schema parser returned something other than a dict, create a default dict
if not isinstance(args, dict):
args = {}
# Generate aggressive values based on type
if prop_type == "string":
args[prop_name] = generate_aggressive_text()
elif prop_type == "integer":
args[prop_name] = _generate_aggressive_integer()
elif prop_type == "number":
args[prop_name] = _generate_aggressive_float()
elif prop_type == "boolean":
# Use non-boolean values to test validation
args[prop_name] = random.choice(
[
None,
"",
"true",
"false",
0,
1,
-1,
42,
[],
{},
"yes",
"no",
"on",
"off",
"True",
"False",
]
)
elif prop_type == "array":
# Generate malformed arrays
aggressive_arrays = [
None, # Not an array
"not_an_array", # String instead
[generate_aggressive_text() for _ in range(random.randint(0, 10))],
[None] * random.randint(1, 5), # Array of nulls
[{}] * random.randint(1, 3), # Array of empty objects
[], # Empty array
["A" * 10000], # Huge string in array
list(range(10000)), # Huge array
]
args[prop_name] = random.choice(aggressive_arrays)
elif prop_type == "object":
# Generate malformed objects
aggressive_objects = [
None, # Not an object
"not_an_object", # String instead
[], # Array instead
{}, # Empty object
{generate_aggressive_text(): generate_aggressive_text()},
{"evil": "<script>alert('xss')</script>"},
{"\x00null\x00": "null_key"},
{"a" * 1000: "huge_key"},
]
args[prop_name] = random.choice(aggressive_objects)
else:
# Fallback for unknown types - try to break them
args[prop_name] = random.choice(
[
None,
generate_aggressive_text(),
_generate_aggressive_integer(),
_generate_aggressive_float(),
[],
{},
]
)
# Ensure we have at least some arguments
if not args and schema.get("properties"):
# Fallback to basic property handling
schema = tool.get("inputSchema")
if not isinstance(schema, dict):
schema = {}
# Use the enhanced schema parser to generate aggressive values
try:
args = make_fuzz_strategy_from_jsonschema(schema, phase="aggressive")
except Exception:
args = {}
# If the schema parser returned something other than a dict, create a default dict
if not isinstance(args, dict):
args = {}
# Ensure we have at least some arguments
if not args and schema.get("properties"):
# Fallback to basic property handling
🤖 Prompt for AI Agents
mcp_fuzzer/fuzz_engine/strategy/aggressive/tool_strategy.py around lines 247 to
261: the code assumes tool.get("inputSchema") is a dict and later calls
schema.get("properties"), which will raise AttributeError if inputSchema is not
a dict; guard by validating the schema type immediately after assignment (e.g.,
if not isinstance(schema, dict): schema = {}), or mirror the realistic
strategy’s type check to coerce non-dict schemas to an empty dict before calling
make_fuzz_strategy_from_jsonschema and before any schema.get(...) calls so
subsequent logic always works with a dict.

Copy link

github-actions bot commented Sep 4, 2025

Documentation Preview

Your documentation changes have been built successfully!

Preview URL: https://Agent-Hellboy.github.io/mcp-server-fuzzer/

The documentation will be automatically deployed to the main site when this PR is merged.


This comment was automatically generated by the documentation workflow.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
.github/workflows/tests.yml (1)

27-27: Expose pytest args, cache pip, and ensure Codecov runs on failure
The mcp_fuzzer/ package exists, so --cov=mcp_fuzzer is correct. Consider:

  • Adding a pytest_args workflow input and passing ${{ inputs.pytest_args }} to pytest for marker/-k flexibility.
  • Enabling cache: 'pip' in actions/setup-python@v5 to speed up CI runs (setup-python supports pip caching) (github.com).
  • Adding if: always() to the Codecov upload step so coverage is uploaded even if tests fail.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f7b4ff2 and 7059ec1.

📒 Files selected for processing (1)
  • .github/workflows/tests.yml (2 hunks)
⏰ 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: component-tests

Copy link

github-actions bot commented Sep 4, 2025

Documentation Preview

Your documentation changes have been built successfully!

Preview URL: https://Agent-Hellboy.github.io/mcp-server-fuzzer/

The documentation will be automatically deployed to the main site when this PR is merged.


This comment was automatically generated by the documentation workflow.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (3)
.github/workflows/component-tests.yml (1)

83-89: Gate Codecov upload on token and files to avoid failures.

Prevents upload on repos without CODECOV_TOKEN or when no reports exist.

-      - name: Upload coverage to Codecov
-        if: ${{ steps.changes.outputs.auth == 'true' || steps.changes.outputs.cli == 'true' || steps.changes.outputs.client == 'true' || steps.changes.outputs.fuzz_engine == 'true' || steps.changes.outputs.safety == 'true' || steps.changes.outputs.transport == 'true' }}
+      - name: Upload coverage to Codecov
+        if: ${{ (steps.changes.outputs.auth == 'true' || steps.changes.outputs.cli == 'true' || steps.changes.outputs.client == 'true' || steps.changes.outputs.fuzz_engine == 'true' || steps.changes.outputs.safety == 'true' || steps.changes.outputs.transport == 'true') && secrets.CODECOV_TOKEN != '' && hashFiles('coverage.*.xml') != '' }}
         uses: codecov/codecov-action@v4
         with:
           token: ${{ secrets.CODECOV_TOKEN }}
           files: coverage.*.xml
           fail_ci_if_error: true
mcp_fuzzer/transport/factory.py (1)

38-66: URL normalization preserves params/query/fragment — nice.

This addresses prior feedback; conversions for sse:// and streamablehttp:// keep full routing info.

mcp_fuzzer/transport/http.py (1)

234-241: Do not await aiter_lines; iterate directly and drop inspect.

Awaiting here is unnecessary and risks TypeError. Simplify to iterate the async iterator.

-                # Iterate over streamed lines; support coroutine-returning aiter_lines
-                lines_iter = response.aiter_lines()
-                if inspect.iscoroutine(lines_iter):
-                    lines_iter = await lines_iter
-                async for line in lines_iter:
+                # Iterate over streamed lines
+                async for line in response.aiter_lines():

Also remove the unused import:

-import inspect
🧹 Nitpick comments (5)
.github/workflows/component-tests.yml (4)

59-82: Add integration tests step to meet PR objectives.

Objectives include integration tests; workflow runs only unit tests. Add an integration step (component-gated if you have marks).

Example (adjust markers/paths as applicable):

+      - name: Run integration tests
+        if: ${{ steps.changes.outputs.auth == 'true' || steps.changes.outputs.cli == 'true' || steps.changes.outputs.client == 'true' || steps.changes.outputs.fuzz_engine == 'true' || steps.changes.outputs.safety == 'true' || steps.changes.outputs.transport == 'true' }}
+        run: |
+          pytest -vv tests/integration --cov-report=xml:coverage.integration.xml

20-23: Enable pip cache for faster runs.

Small speedup with zero behavior change.

       - name: Set up Python
         uses: actions/setup-python@v5
         with:
-          python-version: '3.11'
+          python-version: '3.11'
+          cache: 'pip'

17-18: Either use git history for change detection or drop fetch-depth comment.

You’re not diffing; the “Need full history” note is misleading.

Options:

  • Keep fetch-depth: 0 and add a paths-filter step for PRs.
  • Or remove the comment to avoid confusion.

7-7: Align naming: input says 'safety' but tests path/module are 'safety_system'.

Minor clarity tweak to reduce confusion for operators.

-        description: "Comma-separated components to run (auth,cli,client,fuzz_engine,safety,transport)"
+        description: "Comma-separated components to run (auth,cli,client,fuzz_engine,safety[safety_system],transport)"

Also applies to: 75-77

mcp_fuzzer/transport/streamable_http.py (1)

398-406: Update session headers during streaming.

Capture and persist session headers (e.g., mcp-session-id) before iterating, so downstream requests propagate updated session state.

             try:
-                response.raise_for_status()
+                response.raise_for_status()
+                # Update session headers from streaming response
+                self._maybe_extract_session_headers(response)
                 async for line in response.aiter_lines():
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7059ec1 and 3d451fa.

📒 Files selected for processing (7)
  • .github/workflows/component-tests.yml (1 hunks)
  • mcp_fuzzer/transport/base.py (2 hunks)
  • mcp_fuzzer/transport/factory.py (1 hunks)
  • mcp_fuzzer/transport/http.py (3 hunks)
  • mcp_fuzzer/transport/sse.py (3 hunks)
  • mcp_fuzzer/transport/streamable_http.py (1 hunks)
  • tests/test_invariants.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_invariants.py
🧰 Additional context used
🧬 Code graph analysis (5)
mcp_fuzzer/transport/base.py (5)
tests/unit/transport/test_transport.py (4)
  • AsyncIterator (214-227)
  • _stream_request (53-54)
  • _stream_request (87-88)
  • _stream_request (123-126)
mcp_fuzzer/transport/http.py (1)
  • _stream_request (196-253)
mcp_fuzzer/transport/sse.py (1)
  • _stream_request (68-115)
mcp_fuzzer/transport/streamable_http.py (1)
  • _stream_request (372-415)
mcp_fuzzer/transport/stdio.py (1)
  • _stream_request (243-267)
mcp_fuzzer/transport/streamable_http.py (6)
mcp_fuzzer/transport/base.py (1)
  • _stream_request (53-57)
mcp_fuzzer/transport/http.py (1)
  • _stream_request (196-253)
mcp_fuzzer/transport/sse.py (1)
  • _stream_request (68-115)
mcp_fuzzer/transport/stdio.py (1)
  • _stream_request (243-267)
mcp_fuzzer/safety_system/policy.py (2)
  • is_host_allowed (69-112)
  • sanitize_headers (158-166)
tests/unit/transport/test_streamable_http.py (3)
  • post (78-82)
  • json (38-39)
  • aiter_lines (58-64)
mcp_fuzzer/transport/sse.py (3)
mcp_fuzzer/safety_system/policy.py (2)
  • is_host_allowed (69-112)
  • sanitize_headers (158-166)
mcp_fuzzer/transport/base.py (2)
  • send_raw (21-22)
  • _stream_request (53-57)
mcp_fuzzer/transport/http.py (2)
  • send_raw (125-159)
  • _stream_request (196-253)
mcp_fuzzer/transport/http.py (6)
mcp_fuzzer/transport/stdio.py (2)
  • _update_activity (44-49)
  • _stream_request (243-267)
mcp_fuzzer/transport/base.py (1)
  • _stream_request (53-57)
mcp_fuzzer/transport/sse.py (1)
  • _stream_request (68-115)
mcp_fuzzer/transport/streamable_http.py (1)
  • _stream_request (372-415)
mcp_fuzzer/safety_system/policy.py (2)
  • is_host_allowed (69-112)
  • sanitize_headers (158-166)
tests/unit/transport/test_streamable_http.py (3)
  • post (78-82)
  • json (38-39)
  • aiter_lines (58-64)
mcp_fuzzer/transport/factory.py (4)
mcp_fuzzer/transport/http.py (1)
  • HTTPTransport (23-267)
mcp_fuzzer/transport/streamable_http.py (1)
  • StreamableHTTPTransport (41-415)
mcp_fuzzer/transport/sse.py (1)
  • SSETransport (11-145)
mcp_fuzzer/transport/stdio.py (1)
  • StdioTransport (20-424)
🔇 Additional comments (4)
mcp_fuzzer/transport/streamable_http.py (1)

371-415: Streaming flow looks solid.

Good: policy check, header sanitization, redirect handling with close, robust JSON/SSE line parsing, and guaranteed response close.

mcp_fuzzer/transport/base.py (1)

38-56: Clean abstraction for streaming API.

The default stream_request delegator and abstract _stream_request contract are clear and composable.

mcp_fuzzer/transport/sse.py (1)

23-25: Confirm non-streaming SSE send_request impact
send_request on SSETransport now raises NotImplementedError, which breaks all send*_request methods in mcp_fuzzer/client/protocol_client.py and default get_tools/tools-call in mcp_fuzzer/transport/base.py. Verify no production code instantiates SSETransport for non-streaming requests or update factory/docs to guard against these calls.

mcp_fuzzer/transport/http.py (1)

125-127: Good call updating activity in send_raw.

Keeps activity tracking consistent across code paths.

Copy link

github-actions bot commented Sep 4, 2025

Documentation Preview

Your documentation changes have been built successfully!

Preview URL: https://Agent-Hellboy.github.io/mcp-server-fuzzer/

The documentation will be automatically deployed to the main site when this PR is merged.


This comment was automatically generated by the documentation workflow.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (1)
.github/workflows/tests.yml (1)

39-45: Codecov step will fail on forked PRs due to secret usage; make upload resilient.

Either remove the token (tokenless upload) or skip upload for forked PRs; also run upload even if tests fail to aid debugging. Add fetch-depth: 0 for better PR diff coverage.

-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
@@
-      - name: Upload coverage to Codecov
-        uses: codecov/codecov-action@v4
-        with:
-          token: ${{ secrets.CODECOV_TOKEN }}
-          files: ./coverage.xml
-          fail_ci_if_error: true
+      - name: Upload coverage to Codecov
+        if: ${{ always() && !(github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork) }}
+        uses: codecov/codecov-action@v4
+        with:
+          files: ./coverage.xml
+          fail_ci_if_error: true

If you prefer tokenless for public repos, also remove the token line and (optionally) add:

 permissions:
   contents: read
+  id-token: write
♻️ Duplicate comments (1)
.github/workflows/tests.yml (1)

5-7: Re‑enabled CI triggers — good fix and aligns with earlier review.

push/pull_request on main are back; this addresses the prior “triggers missing” comment.

🧹 Nitpick comments (2)
.github/workflows/tests.yml (2)

15-17: Minimal permissions are correct; consider OIDC if going tokenless with Codecov.

If you drop the Codecov token (recommended for public repos), add id-token: write.

Apply if switching to tokenless:

 permissions:
   contents: read
+  id-token: write

18-20: Refine concurrency key to avoid cross-event collisions.

Using head_ref for PRs prevents canceling unrelated branch runs.

-  group: tests-${{ github.workflow }}-${{ github.ref }}
+  group: tests-${{ github.workflow }}-${{ github.head_ref || github.ref }}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3d451fa and d17897a.

📒 Files selected for processing (1)
  • .github/workflows/tests.yml (2 hunks)
🔇 Additional comments (2)
.github/workflows/tests.yml (2)

8-13: Manual dispatch preserved — LGTM.

Keeping workflow_dispatch with an input is useful for ad‑hoc runs.


38-38: Coverage target confirmed: mcp_fuzzer exists at the repository root and is importable in tests, so the workflow’s --cov=mcp_fuzzer flag is correct.

Copy link
Owner Author

@Agent-Hellboy Agent-Hellboy left a comment

Choose a reason for hiding this comment

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

Summary of applied fixes for CodeRabbit review items:

Implemented (actionable):

  • Workflows:
    • .github/workflows/tests.yml: re-enabled push/pull_request triggers, kept workflow_dispatch, added minimal permissions and concurrency. (Also compatible with pip cache; retained coverage on mcp_fuzzer.)
    • .github/workflows/component-tests.yml: added pip cache, exact token parsing for components input, separate per-component coverage files (coverage.*.xml), Codecov upload gated on token and files, added integration tests step, updated description to clarify safety vs safety_system.
  • Markers and selection:
    • pyproject: unit marker registered; tests/conftest.py already adds component markers at runtime.
    • tests/add_markers.py: longest-prefix match and idempotent merge of existing pytestmark; improved docstring regex.
  • Transport fixes:
    • HTTPTransport._stream_request: iterate response.aiter_lines() directly (no await), removed inspect, ensured response close; safety checks and header sanitization consistent across transports.
    • SSETransport: enforced safety policy with is_host_allowed/sanitize_headers, disabled env proxies and redirects, joined multi-line SSE data with "\n", kept send_request NotImplemented.
    • StreamableHTTPTransport: update session headers during streaming.
    • Base transport: typed stream_request and added abstract _stream_request.
    • Factory: preserved params/query/fragment for sse:// and streamablehttp://; back-compat signature retained.
  • Integration test cleanup:
    • tests/integration/test_client_transport.py: correct httpx/json imports, patch AsyncClient.post, use _send_generic_request, assert two POSTs (initialize + request), optional skip handshake supported.
  • Fuzzer/Schema/Invariant improvements:
    • ProtocolFuzzer: widened types for server_response (dict|list), added BATCH_VALIDATION_TIMEOUT constant, tightened wait_for usage; results include invariant_violations and reflect invariant failures.
    • Schema parser: added const fast-path, intersected allOf types, tuple-items support, stable uniqueItems hashing, type-aware recursion cutoff.
    • Realistic tool strategy: schema-driven arrays/objects, numeric arrays honor constraints, guarded overwrites of format fields, backfill required fields.
    • Aggressive tool strategy: guarded parser errors, backfill required keys, safer defaults; handled non-dict schemas.
    • Invariants: treat request envelopes as invalid in response context; fixed valid notification tests to include method.

Tests and lint:

  • tox: all tests passing locally; ruff clean.

Optional nits (tracked but not blocking):

  • Docs tweaks in docs/reference.md and docs/architecture.md (bullet grammar/numbering) can be addressed in a follow-up docs PR if desired.
  • tests/README.md code-fence language and examples; happy to include in a docs polish commit.

If you want any of the optional documentation nits folded into this PR, I can push those edits as well. Otherwise, from my side, all actionable CodeRabbit comments are resolved.

Copy link

github-actions bot commented Sep 4, 2025

Documentation Preview

Your documentation changes have been built successfully!

Preview URL: https://Agent-Hellboy.github.io/mcp-server-fuzzer/

The documentation will be automatically deployed to the main site when this PR is merged.


This comment was automatically generated by the documentation workflow.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (7)
.github/workflows/component-tests.yml (1)

72-95: Per-component jobs and unique coverage filenames are solid.

Good isolation, clear gating, and no coverage clobbering. Matches prior feedback.

mcp_fuzzer/transport/sse.py (2)

28-33: Good parity: redirects disabled and env proxies ignored.

AsyncClient now sets follow_redirects=False and trust_env=False across all paths; aligns with safety policy and other transports.

Also applies to: 63-67, 85-89


155-157: Spec-correct multi-line join.

Joining data lines with "\n" matches SSE spec. Good catch.

mcp_fuzzer/fuzz_engine/strategy/schema_parser.py (3)

210-237: Respect additionalProperties: false when generating extra properties.

The code correctly checks additionalProperties before adding extra properties in Lines 213-218 and 221-225. Good implementation!


124-130: Excellent implementation of const value handling!

The implementation properly handles const values with appropriate behavior for both realistic and aggressive phases, avoiding returning the const value in aggressive mode when trying to generate violations.


32-60: allOf merge loses restrictive constraints.

The current _merge_allOf function arbitrarily preserves first/last values for non-property/required fields, which can produce invalid schemas when constraints conflict. For example, if one schema has minimum: 10 and another has minimum: 20, the merged schema should use minimum: 20 (the more restrictive value). Similarly, maxLength should take the minimum of all values.

Apply this fix to properly merge numeric and length constraints:

def _merge_allOf(schemas: List[Dict[str, Any]]) -> Dict[str, Any]:
    """Deep merge for allOf schemas."""
    merged: Dict[str, Any] = {}
    props: Dict[str, Any] = {}
    required: List[str] = []
    merged_types = None  # track intersection of declared types
+    # Collect numeric/length constraints for proper merging
+    mins = {"minimum": [], "minLength": [], "minItems": [], "minProperties": []}
+    maxs = {"maximum": [], "maxLength": [], "maxItems": [], "maxProperties": []}
+    exclusive_mins = []
+    exclusive_maxs = []
+    
    for s in schemas:
        if "properties" in s and isinstance(s["properties"], dict):
            props.update(s["properties"])
        if "required" in s and isinstance(s["required"], list):
            required.extend([r for r in s["required"] if isinstance(r, str)])
        t = s.get("type")
        if t is not None:
            tset = set(t if isinstance(t, list) else [t])
            merged_types = tset if merged_types is None else (merged_types & tset)
+        
+        # Collect constraint values
+        for k in mins:
+            if k in s and isinstance(s[k], (int, float)):
+                mins[k].append(s[k])
+        for k in maxs:
+            if k in s and isinstance(s[k], (int, float)):
+                maxs[k].append(s[k])
+        if "exclusiveMinimum" in s:
+            exclusive_mins.append(s["exclusiveMinimum"])
+        if "exclusiveMaximum" in s:
+            exclusive_maxs.append(s["exclusiveMaximum"])
+        
        for k, v in s.items():
-            if k not in ("properties", "required"):
+            if k not in ("properties", "required", "type") and k not in mins and k not in maxs:
                merged[k] = v if k not in merged else merged[k]
+    
+    # Apply most restrictive constraints
+    for k, vals in mins.items():
+        if vals:
+            merged[k] = max(vals)  # Most restrictive minimum
+    for k, vals in maxs.items():
+        if vals:
+            merged[k] = min(vals)  # Most restrictive maximum
+    
+    # Handle exclusive bounds (if any schema sets exclusive, the merged should be exclusive)
+    if exclusive_mins:
+        merged["exclusiveMinimum"] = max(v if isinstance(v, (int, float)) else merged.get("minimum", 0) for v in exclusive_mins)
+    if exclusive_maxs:
+        merged["exclusiveMaximum"] = min(v if isinstance(v, (int, float)) else merged.get("maximum", 0) for v in exclusive_maxs)
+    
    if props:
        merged["properties"] = props
    if required:
        merged["required"] = sorted(set(required))
    if merged_types:
        if len(merged_types) > 1:
            merged["type"] = list(merged_types)
        else:
            merged["type"] = next(iter(merged_types))
    return merged
mcp_fuzzer/fuzz_engine/strategy/aggressive/tool_strategy.py (1)

243-256: Excellent error handling for schema parser integration!

The implementation properly handles parser failures with try/except and includes appropriate type checking for non-dict schemas. This ensures robust fallback behavior when the schema parser encounters issues.

🧹 Nitpick comments (7)
.github/workflows/component-tests.yml (3)

31-64: Optional: auto-detect components when no input is provided.

Objectives mention “only run components that have code changes.” Currently the else-branch runs all. Consider diffing against origin/main to set flags automatically when inputs.components is empty.

Sketch:

CHANGED=$(git diff --name-only origin/main...HEAD || true)
AUTH_CHANGES=$(echo "$CHANGED" | grep -Eq '^mcp_fuzzer/auth/|^tests/(unit/auth|integration)/' && echo true || echo false)
# repeat per component…

15-18: Nit: fetch-depth: 0 is unused.

If you don’t implement git-diff detection, you can drop full history to speed up checkout.


25-30: Nit: consolidate dev deps.

If you have an extras entry (e.g., .[dev]), install once instead of multiple pip installs to save time.

mcp_fuzzer/transport/sse.py (2)

40-57: Handle multi-line SSE events in send_raw, not just first data line.

Current logic may miss/garble payloads split across multiple data: lines. Buffer lines until a blank line, then parse once with _parse_sse_event.

Apply:

-            for line in response.text.splitlines():
-                if line.startswith("data:"):
-                    try:
-                        data = json.loads(line[len("data:") :].strip())
-                        if "error" in data:
-                            raise Exception(f"Server error: {data['error']}")
-                        return data.get("result", data)
-                    except json.JSONDecodeError:
-                        logging.error("Failed to parse SSE data line as JSON")
-                        continue
+            buffer: List[str] = []
+            def flush_once() -> Optional[Dict[str, Any]]:
+                if not buffer:
+                    return None
+                event_text = "\n".join(buffer)
+                buffer.clear()
+                try:
+                    data = SSETransport._parse_sse_event(event_text)
+                except json.JSONDecodeError:
+                    logging.error("Failed to parse SSE data as JSON")
+                    return None
+                if data is None:
+                    return None
+                if "error" in data:
+                    raise Exception(f"Server error: {data['error']}")
+                return data.get("result", data)
+
+            for line in response.text.splitlines():
+                if not line.strip():
+                    result = flush_once()
+                    if result is not None:
+                        return result
+                    continue
+                buffer.append(line)
+            result = flush_once()
+            if result is not None:
+                return result

15-18: Consider explicit SSE-friendly headers.

Add Cache-Control: no-cache to reduce intermediaries’ buffering.

Apply:

         self.headers = {
             "Accept": "text/event-stream",
             "Content-Type": "application/json",
+            "Cache-Control": "no-cache",
         }
mcp_fuzzer/fuzz_engine/strategy/schema_parser.py (1)

342-344: Consider improving error handling specificity.

The bare except Exception on line 342 could mask important errors during pattern generation. Consider catching specific exceptions that _generate_string_from_pattern might raise.

-        except Exception:  # Handle specific exceptions when possible
+        except (ValueError, TypeError, AttributeError):  # Known exceptions from pattern parsing
             # Fallback if pattern generation fails
             pass
mcp_fuzzer/fuzz_engine/strategy/aggressive/tool_strategy.py (1)

156-156: Fix potential string slicing issue for exact length enforcement.

In line 156, when generating digits only, the code uses ("100" * ((length + 2) // 3))[:length] to enforce exact length. However, this might not produce exactly length characters when length is not divisible by 3.

-            ("100" * ((length + 2) // 3))[:length],  # digits only; enforce exact length
+            ("".join(str(i % 10) for i in range(length))),  # digits only; exact length
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d17897a and b4ad42a.

📒 Files selected for processing (5)
  • .github/workflows/component-tests.yml (1 hunks)
  • mcp_fuzzer/fuzz_engine/strategy/aggressive/tool_strategy.py (1 hunks)
  • mcp_fuzzer/fuzz_engine/strategy/schema_parser.py (1 hunks)
  • mcp_fuzzer/transport/sse.py (3 hunks)
  • mcp_fuzzer/transport/streamable_http.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • mcp_fuzzer/transport/streamable_http.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-08T11:50:01.877Z
Learnt from: Agent-Hellboy
PR: Agent-Hellboy/mcp-server-fuzzer#25
File: mcp_fuzzer/strategy/tool_strategies.py:105-139
Timestamp: 2025-08-08T11:50:01.877Z
Learning: Repo Agent-Hellboy/mcp-server-fuzzer: In mcp_fuzzer/strategy/tool_strategies.py, ToolStrategies._generate_aggressive_integer can return None in the "special" branch despite an int annotation. Maintainership decision: ignore for now and handle in issue #18; do not re-flag this as blocking in interim reviews.

Applied to files:

  • mcp_fuzzer/fuzz_engine/strategy/aggressive/tool_strategy.py
🧬 Code graph analysis (2)
mcp_fuzzer/fuzz_engine/strategy/aggressive/tool_strategy.py (1)
mcp_fuzzer/fuzz_engine/strategy/schema_parser.py (1)
  • make_fuzz_strategy_from_jsonschema (62-159)
mcp_fuzzer/transport/sse.py (3)
mcp_fuzzer/transport/base.py (3)
  • TransportProtocol (13-101)
  • send_raw (21-22)
  • _stream_request (53-57)
mcp_fuzzer/safety_system/policy.py (2)
  • is_host_allowed (69-112)
  • sanitize_headers (158-166)
mcp_fuzzer/transport/streamable_http.py (2)
  • send_raw (173-278)
  • _stream_request (372-417)
🪛 actionlint (1.7.7)
.github/workflows/component-tests.yml

102-102: context "secrets" is not allowed here. available contexts are "env", "github", "inputs", "job", "matrix", "needs", "runner", "steps", "strategy", "vars". see https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability for more details

(expression)

⏰ 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: tests
🔇 Additional comments (8)
.github/workflows/component-tests.yml (2)

43-54: Exact token parsing looks good.

Correctly avoids the “cli” vs “client” substring bug and maps safety|safety_system consistently.


96-100: No change required: pytest-cov target already defined in pyproject.toml
The project’s pyproject.toml under [tool.pytest.ini_options] includes --cov=mcp_fuzzer and [tool.coverage.run] specifies the source, so pytest will measure coverage for mcp_fuzzer by default.

mcp_fuzzer/transport/sse.py (1)

23-25: No callers rely on SSETransport.send_request returning data
The only invocation is in test_transport.py, which asserts NotImplementedError—so removing the non-streaming path has no downstream impact.

mcp_fuzzer/fuzz_engine/strategy/schema_parser.py (2)

1-704: Well-structured JSON Schema parser implementation!

The module provides comprehensive support for JSON Schema-based fuzzing with clear separation between "realistic" and "aggressive" modes. The implementation handles edge cases, recursion depth limits, and various schema types properly.


536-547: Robust multipleOf implementation for integers.

The implementation correctly handles the multipleOf constraint by finding valid multiples within the range. The error handling ensures fallback to a valid value even if the calculation fails.

mcp_fuzzer/fuzz_engine/strategy/aggressive/tool_strategy.py (3)

257-260: Good defensive programming with type check.

The additional type check for the parser result ensures that even if the parser returns a non-dict value without raising an exception, the code handles it gracefully.


262-291: Well-designed fallback mechanism for argument generation.

The fallback path provides comprehensive coverage when the schema parser fails or returns empty results. The 80% inclusion probability for optional properties and type-specific generation logic ensures good test coverage even in fallback scenarios.


292-296: Important: Required fields enforcement.

Great addition to ensure that required fields are always present in the generated arguments, even if they were missed during the initial generation or fallback process.

Copy link

github-actions bot commented Sep 4, 2025

Documentation Preview

Your documentation changes have been built successfully!

Preview URL: https://Agent-Hellboy.github.io/mcp-server-fuzzer/

The documentation will be automatically deployed to the main site when this PR is merged.


This comment was automatically generated by the documentation workflow.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (8)
.github/workflows/tests.yml (2)

46-46: Nit: guard Codecov step when coverage.xml is missing.

With if: always(), uploads run even if tests fail and no coverage is produced, causing noisy failures.

Apply:

-      - name: Upload coverage to Codecov
-        if: ${{ always() }}
+      - name: Upload coverage to Codecov
+        if: ${{ always() && hashFiles('coverage.xml') != '' }}
         uses: codecov/codecov-action@v4
         with:
           files: ./coverage.xml
           fail_ci_if_error: true

14-17: Optional: allow marker/selection on PRs too.

pytest_args only works for manual runs. If you want selective runs on PRs based on changed components, consider adding a paths-filter step and pass computed markers via env to pytest.

.github/workflows/component-tests.yml (4)

15-16: Optional: set full fetch depth for better PR context.

Helps coverage upload and potential git-based tooling.

Apply:

-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v4
+        with:
+          fetch-depth: 0

11-13: Optional: add concurrency to avoid duplicate manual runs.

Prevents overlapping component test executions.

Apply:

 jobs:
+  # Avoid duplicate runs of this workflow on the same ref/inputs
+concurrency:
+  group: component-tests-${{ github.workflow }}-${{ github.ref }}-${{ github.event.inputs.components }}
+  cancel-in-progress: true

3-10: Optional: support PR-triggered component selection.

If you want automatic per-component runs on PRs, add pull_request trigger and detect changes with a paths filter, falling back to all on empty matches. Keep workflow_dispatch for manual overrides.


99-104: Optional: make Codecov non-blocking for manual runs.

If uploads fail (e.g., Codecov outage), you may not want to fail the whole manual workflow.

Apply:

       - name: Upload coverage to Codecov
         if: ${{ (steps.changes.outputs.auth == 'true' || steps.changes.outputs.cli == 'true' || steps.changes.outputs.client == 'true' || steps.changes.outputs.fuzz_engine == 'true' || steps.changes.outputs.safety == 'true' || steps.changes.outputs.transport == 'true') && hashFiles('coverage.*.xml') != '' }}
         uses: codecov/codecov-action@v4
         with:
           files: coverage.*.xml
-          fail_ci_if_error: true
+          fail_ci_if_error: false
mcp_fuzzer/types.py (2)

9-9: Introduce a JSON container type alias to clarify intent and avoid repetition.

This field models JSON-RPC single vs. batch responses. A small alias improves readability and keeps usage consistent across the codebase.

-from typing import Any, Dict, List, Optional, TypedDict, Union
+from typing import Any, Dict, List, Optional, TypedDict, Union
+try:
+    from typing import TypeAlias  # py>=3.10
+except ImportError:  # py<3.10
+    from typing_extensions import TypeAlias
+
+# JSON containers (object or array) used for server responses
+JSONContainer: TypeAlias = Union[Dict[str, Any], List[Any]]

23-23: Use structured violation records instead of plain strings.

  • In mcp_fuzzer/types.py change
    invariant_violations: List[str]
    to
    invariant_violations: List[InvariantViolationDict]
    and define a local TypedDict:
    class InvariantViolationDict(TypedDict, total=False):
        id: str
        message: str
        severity: str  # “info” | “warning” | “error”
        path: Optional[str]
        context: Dict[str, Any]
  • In mcp_fuzzer/fuzz_engine/fuzzer/protocol_fuzzer.py update all sites that do
    invariant_violations.extend([str(v) …])
    invariant_violations.append(str(e))
    to build and append instances of InvariantViolationDict instead.
  • If there’s a ProtocolFuzzResult type, add the new invariant_violations field there for consistency with FuzzDataResult.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b4ad42a and ee07f66.

📒 Files selected for processing (4)
  • .github/workflows/component-tests.yml (1 hunks)
  • .github/workflows/tests.yml (1 hunks)
  • mcp_fuzzer/transport/factory.py (1 hunks)
  • mcp_fuzzer/types.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • mcp_fuzzer/transport/factory.py
⏰ 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: tests
🔇 Additional comments (4)
.github/workflows/tests.yml (3)

19-22: Good: OIDC permissions set for tokenless Codecov.

id-token: write enables Codecov v4 OIDC uploads without a repo secret. Nice.


23-25: Good: sensible concurrency guard.

Prevents duplicate runs on the same ref.


31-33: Good: full fetch depth for better coverage attribution.

fetch-depth: 0 helps Codecov with commit parent detection.

.github/workflows/component-tests.yml (1)

29-69: Good: exact token parsing for components.

Switching to tokenized case matching avoids the previous substring bug (cli vs client).

Copy link

github-actions bot commented Sep 4, 2025

Documentation Preview

Your documentation changes have been built successfully!

Preview URL: https://Agent-Hellboy.github.io/mcp-server-fuzzer/

The documentation will be automatically deployed to the main site when this PR is merged.


This comment was automatically generated by the documentation workflow.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
.github/workflows/tests.yml (4)

19-22: Clarify permissions: remove unused id-token or switch fully to tokenless Codecov.

Currently you grant id-token: write but still use a Codecov secret. Choose one:

  • If you’ll keep the secret-based upload, drop id-token.
  • If you want tokenless OIDC (preferred), remove the secret and the probing step below and rely on id-token.

Diff to drop id-token (secret-based path):

 permissions:
   contents: read
-  id-token: write

36-39: Strengthen pip cache key for reliability.

setup-python’s pip cache benefits from explicit dependency paths (pyproject/requirements). Add cache-dependency-path to avoid cache misses.

       uses: actions/setup-python@v5
       with:
         python-version: '3.11'
-        cache: 'pip'
+        cache: 'pip'
+        cache-dependency-path: |
+          pyproject.toml
+          requirements*.txt

45-46: Make pytest_args robust across events.

On push/PR, github.event.inputs may be null. Use an env with a guarded expression, then reference $PYTEST_ARGS.

-      - name: Run tests with coverage
-        run: |
-          pytest -vv --cov=mcp_fuzzer --cov-report=xml ${{ github.event.inputs.pytest_args }}
+      - name: Run tests with coverage
+        env:
+          PYTEST_ARGS: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.pytest_args || '') || '' }}
+        run: |
+          pytest -vv --cov=mcp_fuzzer --cov-report=xml $PYTEST_ARGS

47-56: Simplify Codecov: prefer tokenless OIDC and remove the probing step.

Since permissions include id-token: write, you can drop the secret + check step and rely on OIDC. Keep the fork safeguard. Also consider not failing CI on upload hiccups.

-      - name: Check Codecov token
-        id: codecov_token
-        env:
-          CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
-        run: |
-          if [ -n "$CODECOV_TOKEN" ]; then
-            echo "has_token=true" >> $GITHUB_OUTPUT
-          else
-            echo "has_token=false" >> $GITHUB_OUTPUT
-          fi
       - name: Upload coverage to Codecov
-        if: ${{ steps.codecov_token.outputs.has_token == 'true' && hashFiles('coverage.xml') != '' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) }}
+        if: ${{ hashFiles('coverage.xml') != '' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) }}
         uses: codecov/codecov-action@v4
         with:
-          token: ${{ secrets.CODECOV_TOKEN }}
           files: ./coverage.xml
-          fail_ci_if_error: true
+          use_oidc: true
+          fail_ci_if_error: false

Additional: Pin actions to a commit SHA for supply-chain hardening (instead of @v4).

Also applies to: 58-63

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ee07f66 and 49a4161.

📒 Files selected for processing (1)
  • .github/workflows/tests.yml (1 hunks)
⏰ 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: tests
🔇 Additional comments (3)
.github/workflows/tests.yml (3)

5-17: Triggers and manual inputs look good.

push/pull_request on main plus workflow_dispatch with a pytest_args input aligns with the PR goals for selective runs. No issues here.


23-25: Good use of concurrency.

The group key dedupes runs per branch/PR and cancel-in-progress prevents wasted minutes.


31-33: Checkout with full history: LGTM.

fetch-depth: 0 helps coverage/parent detection and change-based tooling.

Copy link

github-actions bot commented Sep 4, 2025

Documentation Preview

Your documentation changes have been built successfully!

Preview URL: https://Agent-Hellboy.github.io/mcp-server-fuzzer/

The documentation will be automatically deployed to the main site when this PR is merged.


This comment was automatically generated by the documentation workflow.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

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

⚠️ Outside diff range comments (1)
mcp_fuzzer/fuzz_engine/executor.py (1)

171-175: Deadlock risk: double-acquire of self._semaphore in execute_batch + execute().

_bounded_execute_and_track acquires the semaphore, and execute() also acquires it. Under load, tasks will take the first permit and block forever on the second, stalling the batch.

Apply this diff to avoid the double-acquire (keep concurrency enforcement inside execute):

-        async def _bounded_execute_and_track(op, args, kwargs):
-            # Acquire semaphore before execution and release after
-            async with self._semaphore:
-                return await self._execute_and_track(op, args, kwargs)
+        async def _bounded_execute_and_track(op, args, kwargs):
+            # Concurrency is enforced inside execute(); avoid double-acquire deadlock
+            return await self._execute_and_track(op, args, kwargs)

Also applies to: 73-77

♻️ Duplicate comments (3)
mcp_fuzzer/types.py (2)

11-12: Good alias; matches prior guidance.

JSONContainer is clear and centralizes the shape. Consider exporting via all for downstream use.

+# Explicit re-exports (optional)
+__all__ = [
+    "JSONContainer",
+    "FuzzDataResult",
+    "ProtocolFuzzResult",
+    "ToolFuzzResult",
+    "BatchExecutionResult",
+    "SafetyCheckResult",
+    "TransportStats",
+]

20-22: Clear “absent vs None” contract.

The comment resolves earlier ambiguity; signature is correct.

tests/unit/fuzz_engine/fuzzer/test_tool_fuzzer.py (1)

12-13: Register project-wide markers to avoid PytestUnknownMarkWarning.

Add unit, fuzz_engine, fuzzer to pytest config.

Apply in pyproject.toml (or pytest.ini):

 [tool.pytest.ini_options]
-markers = [
-  "auth: tests that exercise authentication",
-]
+markers = [
+  "auth: tests that exercise authentication",
+  "unit: unit tests",
+  "fuzz_engine: fuzz engine component",
+  "fuzzer: tool fuzzer component",
+]

Verify:

#!/bin/bash
fd -a 'pyproject.toml|pytest.ini|tox.ini' -x rg -nC1 -i 'markers|ini_options'
🧹 Nitpick comments (10)
tests/unit/transport/test_transport.py (5)

6-11: Remove unused imports to avoid lint noise.

os, uuid, and call are unused.

-import os
-import uuid
-from unittest.mock import AsyncMock, MagicMock, call, patch
+from unittest.mock import AsyncMock, MagicMock, patch

154-160: Consider asserting exact header values.

Verifying Accept and Content-Type values (e.g., application/json) would harden the contract.


162-176: Rename for accuracy: the test exercises send_raw, not send_request.

-async def test_http_transport_send_request(http_transport):
-    """Test HTTPTransport send_request method."""
+async def test_http_transport_send_raw(http_transport):
+    """Test HTTPTransport send_raw method."""

386-392: Avoid shadow field _sys; the module-level patch is sufficient.

StdioTransport._send_request/_stream_request reference the patched module sys; setting transport._sys is unnecessary.

-        transport = StdioTransport("test_command")
-        transport._sys = mock_sys  # Attach the mock to the transport
+        transport = StdioTransport("test_command")

23-24: Component markers align with the PR’s component-based test strategy.

Consider wiring CI to select tests by component label from changed paths (e.g., pytest -k "transport or common") to satisfy “only run components with code changes”.

tests/unit/fuzz_engine/fuzzer/test_tool_fuzzer.py (3)

86-114: Stabilize test_fuzz_tools to avoid flakiness from safety filter.

Patch is_safe_tool_call to True for this test so “at least some runs were successful” is deterministic.

-    @pytest.mark.asyncio
-    @patch("mcp_fuzzer.fuzz_engine.fuzzer.tool_fuzzer.logging")
-    async def test_fuzz_tools(self, mock_logging):
+    @pytest.mark.asyncio
+    @patch("mcp_fuzzer.fuzz_engine.fuzzer.tool_fuzzer.is_safe_tool_call", return_value=True)
+    @patch("mcp_fuzzer.fuzz_engine.fuzzer.tool_fuzzer.logging")
+    async def test_fuzz_tools(self, mock_logging, mock_is_safe):
         """Test fuzzing multiple tools."""

442-447: Make log assertion less brittle.

Avoid stringifying Call objects; inspect args directly.

-            calls = [str(c) for c in mock_logger.info.call_args_list]
-            self.assertTrue(any("test_tool" in c for c in calls))
+            calls = mock_logger.info.call_args_list
+            self.assertTrue(any(("test_tool" in (c.args[0] if c.args else "")) for c in calls))

519-522: Close executors to prevent resource leakage.

Shut down fuzzer executors in this test.

-        results1 = await fuzzer1.fuzz_tool(tool, runs=1)
-        results2 = await fuzzer2.fuzz_tool(tool, runs=1)
+        results1 = await fuzzer1.fuzz_tool(tool, runs=1)
+        results2 = await fuzzer2.fuzz_tool(tool, runs=1)
+        await fuzzer1.shutdown()
+        await fuzzer2.shutdown()
mcp_fuzzer/fuzz_engine/strategy/schema_parser.py (2)

149-163: Recursion cap off-by-one.

Use >= so MAX_RECURSION_DEPTH is the true ceiling.

-    if recursion_depth > MAX_RECURSION_DEPTH:
+    if recursion_depth >= MAX_RECURSION_DEPTH:

267-289: Honor maxProperties.

Currently ignored; clamp extras in realistic mode to not exceed maxProperties.

     # Ensure we meet minProperties constraint
@@
     # In aggressive mode, sometimes add extra properties (if allowed)
@@
-    return result
+    # Respect maxProperties if present (best-effort)
+    max_props = schema.get("maxProperties")
+    if isinstance(max_props, int) and max_props >= 0 and len(result) > max_props:
+        # Keep required first when trimming
+        required_set = set(required)
+        ordered = dict(
+            list((k, v) for k, v in result.items() if k in required_set)
+            + list((k, v) for k, v in result.items() if k not in required_set)
+        )
+        result = dict(list(ordered.items())[:max_props])
+    return result
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 49a4161 and 7904614.

📒 Files selected for processing (8)
  • .github/workflows/component-tests.yml (1 hunks)
  • docs/reference.md (2 hunks)
  • mcp_fuzzer/fuzz_engine/executor.py (1 hunks)
  • mcp_fuzzer/fuzz_engine/strategy/schema_parser.py (1 hunks)
  • mcp_fuzzer/transport/sse.py (3 hunks)
  • mcp_fuzzer/types.py (1 hunks)
  • tests/unit/fuzz_engine/fuzzer/test_tool_fuzzer.py (13 hunks)
  • tests/unit/transport/test_transport.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • docs/reference.md
  • .github/workflows/component-tests.yml
  • mcp_fuzzer/transport/sse.py
🧰 Additional context used
🧬 Code graph analysis (2)
tests/unit/transport/test_transport.py (6)
tests/unit/transport/test_streamable_http.py (4)
  • json (38-39)
  • aiter_lines (58-64)
  • raise_for_status (34-36)
  • raise_for_status (55-56)
mcp_fuzzer/transport/http.py (5)
  • HTTPTransport (23-267)
  • send_request (71-123)
  • send_raw (125-159)
  • send_notification (161-184)
  • _stream_request (196-253)
mcp_fuzzer/transport/sse.py (6)
  • SSETransport (11-198)
  • send_request (20-25)
  • send_raw (27-57)
  • send_notification (59-74)
  • _stream_request (76-168)
  • _parse_sse_event (171-198)
mcp_fuzzer/transport/stdio.py (6)
  • StdioTransport (20-424)
  • send_request (172-196)
  • send_raw (198-211)
  • send_notification (232-241)
  • _send_request (213-230)
  • _stream_request (243-267)
mcp_fuzzer/transport/base.py (8)
  • TransportProtocol (13-101)
  • send_request (15-18)
  • send_raw (21-22)
  • send_notification (25-28)
  • _stream_request (53-57)
  • connect (30-32)
  • disconnect (34-36)
  • stream_request (38-50)
mcp_fuzzer/transport/factory.py (1)
  • create_transport (9-75)
tests/unit/fuzz_engine/fuzzer/test_tool_fuzzer.py (2)
mcp_fuzzer/fuzz_engine/fuzzer/tool_fuzzer.py (1)
  • ToolFuzzer (21-257)
mcp_fuzzer/fuzz_engine/strategy/strategy_manager.py (1)
  • fuzz_tool_arguments (54-61)
⏰ 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: tests
🔇 Additional comments (8)
tests/unit/transport/test_transport.py (6)

27-33: Abstract base checks look good.


39-63: Connection no-op coverage is fine.


479-491: Factory HTTP/HTTPS coverage looks good.


493-498: SSE factory mapping assertion is solid.


500-504: Use a non-empty stdio command to avoid ambiguous semantics.

Some implementations require a command; stdio: may be treated as empty.

-def test_create_transport_stdio():
+def test_create_transport_stdio():
     """Test create_transport with stdio URL."""
-    transport = create_transport("stdio:")
+    transport = create_transport("stdio://python")
     assert isinstance(transport, StdioTransport)

Would you like me to add an assertion on the parsed command (e.g., equals "python")?


506-509: Invalid scheme test is correct.

mcp_fuzzer/types.py (1)

27-27: Invariant_violations always a List[str]
Per-run and default result paths initialize invariant_violations = [] and only extend or assign a List[str]; omission when empty aligns with total=False.

tests/unit/fuzz_engine/fuzzer/test_tool_fuzzer.py (1)

130-149: AsyncMock usage LGTM.

Side effect coroutine matches fuzz_tool signature; covers error path cleanly.

Comment on lines +245 to +249
# Proactively cancel outstanding tasks and wait for them to finish
for task in list(self._running_tasks):
task.cancel()
await asyncio.gather(*self._running_tasks, return_exceptions=True)
finally:
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Cancel-then-await can hang indefinitely; bound the second wait.

If tasks ignore cancellation or are stuck in uninterruptible awaits, the unconditional gather() can block shutdown forever.

Apply this diff to add a bounded “grace” wait and log if tasks remain:

             for task in list(self._running_tasks):
                 task.cancel()
-            await asyncio.gather(*self._running_tasks, return_exceptions=True)
+            try:
+                await asyncio.wait_for(
+                    asyncio.gather(*self._running_tasks, return_exceptions=True),
+                    timeout=min(2.0, timeout),
+                )
+            except asyncio.TimeoutError:
+                self._logger.error(
+                    "Forced shutdown still timed out; %d tasks may still be running",
+                    sum(1 for t in self._running_tasks if not t.done()),
+                )
📝 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
# Proactively cancel outstanding tasks and wait for them to finish
for task in list(self._running_tasks):
task.cancel()
await asyncio.gather(*self._running_tasks, return_exceptions=True)
finally:
# Proactively cancel outstanding tasks and wait for them to finish
for task in list(self._running_tasks):
task.cancel()
try:
await asyncio.wait_for(
asyncio.gather(*self._running_tasks, return_exceptions=True),
timeout=min(2.0, timeout),
)
except asyncio.TimeoutError:
self._logger.error(
"Forced shutdown still timed out; %d tasks may still be running",
sum(1 for t in self._running_tasks if not t.done()),
)
finally:

Comment on lines +250 to +253
# Ensure the set is cleaned up
self._running_tasks = {
t for t in self._running_tasks if not t.cancelled() and not t.done()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Rebinding _running_tasks breaks done callbacks; mutate in place instead.

add_done_callback(self._running_tasks.discard) captures the old set object. Reassigning self._running_tasks creates a new set, so callbacks remove from the stale set, leaking entries in the current set.

Apply this diff to preserve the set identity and safely remove completed/cancelled tasks:

-            # Ensure the set is cleaned up
-            self._running_tasks = {
-                t for t in self._running_tasks if not t.cancelled() and not t.done()
-            }
+            # Ensure the set is cleaned up without rebinding (callbacks rely on identity)
+            to_remove = {t for t in tuple(self._running_tasks) if t.done() or t.cancelled()}
+            self._running_tasks.difference_update(to_remove)
📝 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
# Ensure the set is cleaned up
self._running_tasks = {
t for t in self._running_tasks if not t.cancelled() and not t.done()
}
# Ensure the set is cleaned up without rebinding (callbacks rely on identity)
to_remove = {t for t in tuple(self._running_tasks) if t.done() or t.cancelled()}
self._running_tasks.difference_update(to_remove)
🤖 Prompt for AI Agents
mcp_fuzzer/fuzz_engine/executor.py around lines 250-253: reassigning
self._running_tasks to a new set breaks existing
add_done_callback(self._running_tasks.discard) callbacks because they hold a
reference to the old set; instead mutate the existing set in place to preserve
identity. Replace the reassignment with an in-place removal of
completed/cancelled tasks (e.g., use set.difference_update(...) or iterate and
call self._running_tasks.discard(...) for each finished task) so callbacks
remove items from the live set and no entries are leaked.

Comment on lines +32 to +129
def _merge_allOf(schemas: List[Dict[str, Any]]) -> Dict[str, Any]:
"""
Deep merge for allOf schemas.
Combines properties and required fields, intersects types,
and applies the most restrictive constraints (max of minimums, min of maximums).
"""
merged: Dict[str, Any] = {}
props: Dict[str, Any] = {}
required: List[str] = []
merged_types = None # track intersection of declared types

# Track min/max constraint values
min_constraints = {
"minLength": None,
"minItems": None,
"minProperties": None,
"minimum": None,
"exclusiveMinimum": None,
}
max_constraints = {
"maxLength": None,
"maxItems": None,
"maxProperties": None,
"maximum": None,
"exclusiveMaximum": None,
}

for s in schemas:
# Merge properties
if "properties" in s and isinstance(s["properties"], dict):
props.update(s["properties"])

# Merge required fields
if "required" in s and isinstance(s["required"], list):
required.extend([r for r in s["required"] if isinstance(r, str)])

# Intersect types
t = s.get("type")
if t is not None:
tset = set(t if isinstance(t, list) else [t])
merged_types = tset if merged_types is None else (merged_types & tset)

# Handle const values
if "const" in s:
merged["const"] = s["const"]

# Track min constraints (take maximum value)
for key in min_constraints:
if key in s:
current = min_constraints[key]
new_val = s[key]
if current is None or (new_val is not None and new_val > current):
min_constraints[key] = new_val

# Track max constraints (take minimum value)
for key in max_constraints:
if key in s:
current = max_constraints[key]
new_val = s[key]
if current is None or (new_val is not None and new_val < current):
max_constraints[key] = new_val

# Copy other fields (non-constraint)
for k, v in s.items():
if (
k not in ("properties", "required", "type", "const")
and k not in min_constraints
and k not in max_constraints
):
merged[k] = v if k not in merged else merged[k]

# Apply merged properties
if props:
merged["properties"] = props

# Apply merged required fields
if required:
merged["required"] = sorted(set(required))

# Apply merged types
if merged_types:
if len(merged_types) > 1:
merged["type"] = list(merged_types)
else:
merged["type"] = next(iter(merged_types))

# Apply min constraints
for key, value in min_constraints.items():
if value is not None:
merged[key] = value

# Apply max constraints
for key, value in max_constraints.items():
if value is not None:
merged[key] = value

return merged
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

allOf merge: fix const conflicts and exclusive merging; avoid mixed-type errors.*

Current merge overwrites const and mixes bool/number exclusive*; can yield invalid schemas.

-    # Track min/max constraint values
-    min_constraints = {
-        "minLength": None,
-        "minItems": None,
-        "minProperties": None,
-        "minimum": None,
-        "exclusiveMinimum": None,
-    }
-    max_constraints = {
-        "maxLength": None,
-        "maxItems": None,
-        "maxProperties": None,
-        "maximum": None,
-        "exclusiveMaximum": None,
-    }
+    # Track min/max constraint values (exclusive* handled separately)
+    min_constraints = {"minLength": None, "minItems": None, "minProperties": None, "minimum": None}
+    max_constraints = {"maxLength": None, "maxItems": None, "maxProperties": None, "maximum": None}
+    # Track exclusives (draft-04 booleans vs draft-06+ numbers)
+    exc_min_bool = False
+    exc_max_bool = False
+    exc_min_num: Optional[float] = None
+    exc_max_num: Optional[float] = None
+    # Track consts to detect conflicts
+    const_values: List[Any] = []
@@
-        # Handle const values
-        if "const" in s:
-            merged["const"] = s["const"]
+        # Collect const values
+        if "const" in s:
+            const_values.append(s["const"])
@@
-        # Track min constraints (take maximum value)
+        # Track min constraints (take maximum value)
         for key in min_constraints:
             if key in s:
                 current = min_constraints[key]
                 new_val = s[key]
                 if current is None or (new_val is not None and new_val > current):
                     min_constraints[key] = new_val
@@
-        # Track max constraints (take minimum value)
+        # Track max constraints (take minimum value)
         for key in max_constraints:
             if key in s:
                 current = max_constraints[key]
                 new_val = s[key]
                 if current is None or (new_val is not None and new_val < current):
                     max_constraints[key] = new_val
+
+        # Track exclusiveMinimum/Maximum
+        em = s.get("exclusiveMinimum", None)
+        if isinstance(em, bool) and em:
+            exc_min_bool = True
+        elif isinstance(em, (int, float)):
+            exc_min_num = float(em) if exc_min_num is None else max(exc_min_num, float(em))
+        ex = s.get("exclusiveMaximum", None)
+        if isinstance(ex, bool) and ex:
+            exc_max_bool = True
+        elif isinstance(ex, (int, float)):
+            exc_max_num = float(ex) if exc_max_num is None else min(exc_max_num, float(ex))
@@
     # Apply min constraints
     for key, value in min_constraints.items():
         if value is not None:
             merged[key] = value
@@
     # Apply max constraints
     for key, value in max_constraints.items():
         if value is not None:
             merged[key] = value
+
+    # Apply exclusives with stricter interpretation
+    if exc_min_num is not None:
+        merged["exclusiveMinimum"] = exc_min_num
+    elif exc_min_bool:
+        merged["exclusiveMinimum"] = True
+    if exc_max_num is not None:
+        merged["exclusiveMaximum"] = exc_max_num
+    elif exc_max_bool:
+        merged["exclusiveMaximum"] = True
+
+    # Apply const only if all consts agree
+    if const_values:
+        if len(set(map(repr, const_values))) == 1:
+            merged["const"] = const_values[0]
+        else:
+            # conflicting consts => unsatisfiable; omit const to allow aggressive generation
+            merged.pop("const", None)

Also, if merged_types becomes an empty set, consider signaling unsatisfiable (e.g., store merged["type"] = [] or set a marker) so callers can choose aggressive fallback.


🏁 Script executed:

#!/bin/bash
set -e

# Locate the schema_parser file
echo "Locating schema_parser.py:"
fd -g schema_parser.py

# Show the _merge_allOf function definition
echo "Showing _merge_allOf implementation:"
rg -nP 'def _merge_allOf' -C5 mcp_fuzzer/fuzz_engine/strategy/schema_parser.py

# Search tests for allOf usage
echo "Tests referencing allOf:"
rg -n 'allOf' -C2 .

# Search tests for merge_allOf usage
echo "Tests referencing merge_allOf:"
rg -n 'merge_allOf' -C2 .

Length of output: 6960


🏁 Script executed:

#!/bin/bash
set -e

echo "Code references to exclusiveMinimum:"
rg -n 'exclusiveMinimum' -C2 mcp_fuzzer/fuzz_engine/strategy/schema_parser.py || true

echo "Test references to exclusiveMinimum:"
rg -n 'exclusiveMinimum' -C2 tests || true

echo "Code references to const:"
rg -n 'const' -C2 mcp_fuzzer/fuzz_engine/strategy/schema_parser.py || true

echo "Test references to const:"
rg -n 'const' -C2 tests || true

Length of output: 19655


Refactor _merge_allOf to correctly handle const conflicts and exclusiveMinimum/exclusiveMaximum

  • Separate exclusiveMinimum/exclusiveMaximum from the general min/max maps: track booleans vs numeric values and apply the strictest numeric bounds when present (currently allOf treats exclusive* like other constraints and may mix bool/number)
  • Aggregate all const values across schemas and only emit merged["const"] if they’re identical; drop or mark unsatisfiable on conflict (currently later schemas overwrite earlier ones)
  • Detect when merged_types becomes empty and signal unsatisfiable (e.g. set type: [] or use a marker) so callers can choose an aggressive fallback
  • Add unit tests covering allOf merges with conflicting const values and mixed exclusiveMinimum/exclusiveMaximum scenarios
🤖 Prompt for AI Agents
In mcp_fuzzer/fuzz_engine/strategy/schema_parser.py around lines 32-129,
_merge_allOf currently overwrites consts, treats
exclusiveMinimum/exclusiveMaximum like numeric bounds, and silently allows an
empty type intersection; change it to (1) collect all const values seen and only
set merged["const"] if all are identical, otherwise set an explicit
unsatisfiable marker (e.g. merged["_unsatisfiable"]=True or drop const and
record conflict) so callers can handle it; (2) separate handling for
exclusiveMinimum/exclusiveMaximum by tracking both a numeric bound and a boolean
strict flag for each schema, then when merging choose the most restrictive
numeric limit and set exclusive* True if any schema requires strictness; (3)
after intersecting types, detect when merged_types becomes empty and mark merged
as unsatisfiable (e.g. merged["type"]=[] or merged["_unsatisfiable"]=True) so
callers can make an aggressive fallback; and (4) add unit tests that cover
conflicting consts, mixed exclusiveMinimum/exclusiveMaximum (number vs boolean),
and empty type intersections to validate behavior.

Comment on lines +292 to +306
phase == "aggressive"
and random.random() < 0.3
and schema.get("additionalProperties", True) is not False
):
# Add some potentially problematic properties
extra_props = {
"__proto__": {"isAdmin": True},
"constructor": {"prototype": {"isAdmin": True}},
"eval": "console.log('code injection')",
}
result.update(
random.sample(
extra_props.items(), k=min(len(extra_props), random.randint(1, 3))
)
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

random.sample on dict_items may TypeError; convert to list.

Ensure compatibility across Python versions.

-        result.update(
-            random.sample(
-                extra_props.items(), k=min(len(extra_props), random.randint(1, 3))
-            )
-        )
+        pool = list(extra_props.items())
+        result.update(
+            random.sample(pool, k=min(len(pool), random.randint(1, 3)))
+        )
📝 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
phase == "aggressive"
and random.random() < 0.3
and schema.get("additionalProperties", True) is not False
):
# Add some potentially problematic properties
extra_props = {
"__proto__": {"isAdmin": True},
"constructor": {"prototype": {"isAdmin": True}},
"eval": "console.log('code injection')",
}
result.update(
random.sample(
extra_props.items(), k=min(len(extra_props), random.randint(1, 3))
)
)
if (
phase == "aggressive"
and random.random() < 0.3
and schema.get("additionalProperties", True) is not False
):
# Add some potentially problematic properties
extra_props = {
"__proto__": {"isAdmin": True},
"constructor": {"prototype": {"isAdmin": True}},
"eval": "console.log('code injection')",
}
pool = list(extra_props.items())
result.update(
random.sample(pool, k=min(len(pool), random.randint(1, 3)))
)
🤖 Prompt for AI Agents
In mcp_fuzzer/fuzz_engine/strategy/schema_parser.py around lines 292 to 306, the
call to random.sample(extra_props.items(), ...) can raise a TypeError on some
Python versions because dict_items is not accepted by random.sample; convert the
dict_items to a list before sampling. Replace the random.sample call argument
with list(extra_props.items()) (i.e., random.sample(list(extra_props.items()),
k=...)) to ensure compatibility across Python versions.

Comment on lines +168 to +176
with patch.object(httpx.AsyncClient, "post") as mock_post:
mock_response = MagicMock()
mock_response.json.return_value = test_response
mock_response.status_code = 200
mock_post.return_value = mock_response

# Test send_request
result = await http_transport.send_raw(test_payload)

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Patch async httpx.AsyncClient.post with AsyncMock; MagicMock is not awaitable.

AsyncClient.post is awaited in the implementation. Patch it with new_callable=AsyncMock to prevent “object MagicMock is not awaitable”.

-    with patch.object(httpx.AsyncClient, "post") as mock_post:
+    with patch.object(httpx.AsyncClient, "post", new_callable=AsyncMock) as mock_post:
📝 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
with patch.object(httpx.AsyncClient, "post") as mock_post:
mock_response = MagicMock()
mock_response.json.return_value = test_response
mock_response.status_code = 200
mock_post.return_value = mock_response
# Test send_request
result = await http_transport.send_raw(test_payload)
with patch.object(httpx.AsyncClient, "post", new_callable=AsyncMock) as mock_post:
mock_response = MagicMock()
mock_response.json.return_value = test_response
mock_response.status_code = 200
mock_post.return_value = mock_response
# Test send_request
result = await http_transport.send_raw(test_payload)
🤖 Prompt for AI Agents
In tests/unit/transport/test_transport.py around lines 168 to 176, the test
patches httpx.AsyncClient.post with a MagicMock which is not awaitable; replace
the patch call to use new_callable=AsyncMock (from unittest.mock) so post is
awaitable, keep the MagicMock response and set mock_post.return_value =
mock_response (or set mock_post.return_value.json.return_value = test_response)
so awaiting post returns the prepared response.

Comment on lines +191 to +197
with patch.object(httpx.AsyncClient, "post") as mock_post:
mock_post.side_effect = httpx.RequestError("Connection error")

# Test send_request with error
with pytest.raises(httpx.RequestError):
await http_transport.send_raw(test_payload)

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Same awaitability issue for error-path patch.

-    with patch.object(httpx.AsyncClient, "post") as mock_post:
+    with patch.object(httpx.AsyncClient, "post", new_callable=AsyncMock) as mock_post:
📝 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
with patch.object(httpx.AsyncClient, "post") as mock_post:
mock_post.side_effect = httpx.RequestError("Connection error")
# Test send_request with error
with pytest.raises(httpx.RequestError):
await http_transport.send_raw(test_payload)
with patch.object(httpx.AsyncClient, "post", new_callable=AsyncMock) as mock_post:
mock_post.side_effect = httpx.RequestError("Connection error")
# Test send_request with error
with pytest.raises(httpx.RequestError):
await http_transport.send_raw(test_payload)
🤖 Prompt for AI Agents
In tests/unit/transport/test_transport.py around lines 191 to 197, the test
patches httpx.AsyncClient.post but uses a regular Mock so awaiting it in the
code under test fails; make the patch awaitable by patching with an AsyncMock
(or use new_callable=AsyncMock) or set mock_post.side_effect to an async
function that raises httpx.RequestError, then run the test expecting the
exception as before.

Comment on lines +208 to +216
with patch.object(httpx.AsyncClient, "post") as mock_post:
# Create a proper AsyncMock for the response
mock_response = AsyncMock()

# Create a simpler mock for the async iterator
async def mock_aiter_lines():
class AsyncIterator:
def __init__(self, items):
self.items = items
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix stream test: post should be AsyncMock; raise_for_status should be sync; ensure aclose is async.

  • Patch post with AsyncMock.
  • Set raise_for_status to MagicMock() (httpx has sync method); asserting an awaited call risks coroutine-not-awaited warnings.
-    with patch.object(httpx.AsyncClient, "post") as mock_post:
+    with patch.object(httpx.AsyncClient, "post", new_callable=AsyncMock) as mock_post:
@@
-        # Add raise_for_status method
-        mock_response.raise_for_status = AsyncMock()
+        # Add raise_for_status method (sync in httpx)
+        mock_response.raise_for_status = MagicMock()
+        # Ensure aclose is awaitable
+        mock_response.aclose = AsyncMock()
@@
-        # raise_for_status in httpx is sync on Response; our code calls it sync
-        # so assert it was called (not awaited)
-        assert mock_response.raise_for_status.called
+        mock_response.raise_for_status.assert_called_once()

Also applies to: 236-257

🤖 Prompt for AI Agents
In tests/unit/transport/test_transport.py around lines 208-216 (and similarly
236-257), the test patches httpx.AsyncClient.post incorrectly and configures
response methods as coroutines; change the patch to use AsyncMock for post, set
mock_response.raise_for_status = MagicMock() (since httpx.raise_for_status is
synchronous) and ensure mock_response.aclose is an AsyncMock(), and keep the
async iterator for .aiter_lines as an async generator/iterator; update
assertions to await only actual async calls (post, aclose) and not
raise_for_status.

Comment on lines +325 to +341
with patch.object(httpx.AsyncClient, "stream") as mock_stream:
# Mock streaming response
mock_response = MagicMock()
mock_response.aiter_text.return_value = sse_events
mock_stream.return_value.__aenter__.return_value = mock_response

# Test stream_request
responses = []
async for response in sse_transport._stream_request(test_payload):
responses.append(response)

# Check the results
assert len(responses) == 2
assert responses[0] == {"id": 1, "result": "streaming"}
assert responses[1] == {"id": 2, "result": "complete"}
mock_stream.assert_called_once()

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Make httpx.AsyncClient.stream an async context manager and aiter_text() async-iterable.

Current setup returns a plain list for aiter_text(), which is not async-iterable, and stream is not an async CM. Use an async contextmanager and async generator.

-    with patch.object(httpx.AsyncClient, "stream") as mock_stream:
-        # Mock streaming response
-        mock_response = MagicMock()
-        mock_response.aiter_text.return_value = sse_events
-        mock_stream.return_value.__aenter__.return_value = mock_response
+    from contextlib import asynccontextmanager
+    with patch.object(httpx.AsyncClient, "stream") as mock_stream:
+        mock_response = MagicMock()
+        async def aiter_text():
+            for ev in sse_events:
+                yield ev
+        mock_response.aiter_text.return_value = aiter_text()
+        mock_response.raise_for_status = MagicMock()
+        @asynccontextmanager
+        async def stream_ctx(*_args, **_kwargs):
+            yield mock_response
+        mock_stream.return_value = stream_ctx()
📝 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
with patch.object(httpx.AsyncClient, "stream") as mock_stream:
# Mock streaming response
mock_response = MagicMock()
mock_response.aiter_text.return_value = sse_events
mock_stream.return_value.__aenter__.return_value = mock_response
# Test stream_request
responses = []
async for response in sse_transport._stream_request(test_payload):
responses.append(response)
# Check the results
assert len(responses) == 2
assert responses[0] == {"id": 1, "result": "streaming"}
assert responses[1] == {"id": 2, "result": "complete"}
mock_stream.assert_called_once()
from contextlib import asynccontextmanager
with patch.object(httpx.AsyncClient, "stream") as mock_stream:
mock_response = MagicMock()
async def aiter_text():
for ev in sse_events:
yield ev
mock_response.aiter_text.return_value = aiter_text()
mock_response.raise_for_status = MagicMock()
@asynccontextmanager
async def stream_ctx(*_args, **_kwargs):
yield mock_response
mock_stream.return_value = stream_ctx()
# Test stream_request
responses = []
async for response in sse_transport._stream_request(test_payload):
responses.append(response)
# Check the results
assert len(responses) == 2
assert responses[0] == {"id": 1, "result": "streaming"}
assert responses[1] == {"id": 2, "result": "complete"}
mock_stream.assert_called_once()

Comment on lines +348 to +354
with patch.object(httpx.AsyncClient, "stream") as mock_stream:
mock_stream.side_effect = httpx.RequestError("Connection error")

# Test stream_request with error
with pytest.raises(httpx.RequestError):
async for _ in sse_transport._stream_request(test_payload):
pass
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Raise error from the async context manager instead of the function patch.

Ensure the failure occurs on entering the async with block.

-    with patch.object(httpx.AsyncClient, "stream") as mock_stream:
-        mock_stream.side_effect = httpx.RequestError("Connection error")
+    from contextlib import asynccontextmanager
+    with patch.object(httpx.AsyncClient, "stream") as mock_stream:
+        @asynccontextmanager
+        async def stream_ctx(*_args, **_kwargs):
+            raise httpx.RequestError("Connection error")
+            yield  # unreachable
+        mock_stream.return_value = stream_ctx()
📝 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
with patch.object(httpx.AsyncClient, "stream") as mock_stream:
mock_stream.side_effect = httpx.RequestError("Connection error")
# Test stream_request with error
with pytest.raises(httpx.RequestError):
async for _ in sse_transport._stream_request(test_payload):
pass
from contextlib import asynccontextmanager
with patch.object(httpx.AsyncClient, "stream") as mock_stream:
@asynccontextmanager
async def stream_ctx(*_args, **_kwargs):
raise httpx.RequestError("Connection error")
yield # unreachable
mock_stream.return_value = stream_ctx()
# Test stream_request with error
with pytest.raises(httpx.RequestError):
async for _ in sse_transport._stream_request(test_payload):
pass
🤖 Prompt for AI Agents
In tests/unit/transport/test_transport.py around lines 348 to 354, the patch
currently makes httpx.RequestError raise when the stream function is called, but
the test needs the error to be raised when entering the async context manager;
update the mock so the async context manager returned by
httpx.AsyncClient.stream raises on __aenter__ (e.g., set
mock_stream.return_value.__aenter__.side_effect to
httpx.RequestError("Connection error")), then keep the existing pytest.raises
block and async for to ensure the failure happens on entering the async with.

Copy link

github-actions bot commented Sep 4, 2025

Documentation Preview

Your documentation changes have been built successfully!

Preview URL: https://Agent-Hellboy.github.io/mcp-server-fuzzer/

The documentation will be automatically deployed to the main site when this PR is merged.


This comment was automatically generated by the documentation workflow.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (3)
mcp_fuzzer/transport/sse.py (3)

28-32: Good hardening: no env proxies and no implicit redirects.

trust_env=False and follow_redirects=False align this transport with safety policy.

Also applies to: 81-85, 103-107


188-217: SSE spec compliance: joining multi-line data with “\n”.

Correct and matches spec; prevents payload corruption.


121-177: Critical: chunk-boundary bug — aiter_text can split lines mid-field. Use aiter_lines with event buffering.

Current logic can corrupt lines across text chunks, causing JSON parse failures or dropped events.

-                chunks = response.aiter_text()
-                buffer = []  # Buffer to accumulate SSE event data
+                lines = response.aiter_lines()
+                event_lines: List[str] = []
+
+                def flush_event() -> Optional[Dict[str, Any]]:
+                    if not event_lines:
+                        return None
+                    event_text = "\n".join(event_lines)
+                    event_lines.clear()
+                    try:
+                        parsed = SSETransport._parse_sse_event(event_text)
+                    except json.JSONDecodeError:
+                        logging.error("Failed to parse SSE event payload as JSON")
+                        return None
+                    return parsed
@@
-                if hasattr(chunks, "__aiter__"):
-                    async for chunk in chunks:  # type: ignore[func-returns-value]
-                        if not chunk:
-                            continue
-
-                        # Process each line in the chunk
-                        for line in chunk.splitlines():
-                            if line.strip():
-                                # Non-empty line: add to current event buffer
-                                buffer.append(line)
-                            else:
-                                # Empty line: marks end of an event, process the buffer
-                                if buffer:
-                                    try:
-                                        event_text = "\n".join(buffer)
-                                        parsed = SSETransport._parse_sse_event(
-                                            event_text
-                                        )
-                                        if parsed is not None:
-                                            yield parsed
-                                    except json.JSONDecodeError:
-                                        logging.error(
-                                            "Failed to parse SSE event payload as JSON"
-                                        )
-                                    finally:
-                                        buffer = []  # Clear buffer for next event
+                if hasattr(lines, "__aiter__"):
+                    async for line in lines:  # type: ignore[func-returns-value]
+                        if not line.strip():
+                            parsed = flush_event()
+                            if parsed is not None:
+                                yield parsed
+                            continue
+                        event_lines.append(line)
                 else:
-                    for chunk in chunks:  # type: ignore[assignment]
-                        if not chunk:
-                            continue
-
-                        # Process each line in the chunk
-                        for line in chunk.splitlines():
-                            if line.strip():
-                                # Non-empty line: add to current event buffer
-                                buffer.append(line)
-                            else:
-                                # Empty line: marks end of an event, process the buffer
-                                if buffer:
-                                    try:
-                                        event_text = "\n".join(buffer)
-                                        parsed = SSETransport._parse_sse_event(
-                                            event_text
-                                        )
-                                        if parsed is not None:
-                                            yield parsed
-                                    except json.JSONDecodeError:
-                                        logging.error(
-                                            "Failed to parse SSE event payload as JSON"
-                                        )
-                                    finally:
-                                        buffer = []  # Clear buffer for next event
+                    for line in lines:  # type: ignore[assignment]
+                        if not line.strip():
+                            parsed = flush_event()
+                            if parsed is not None:
+                                yield parsed
+                            continue
+                        event_lines.append(line)
@@
-                # Process any remaining buffered data at the end of the stream
-                if buffer:
-                    try:
-                        event_text = "\n".join(buffer)
-                        parsed = SSETransport._parse_sse_event(event_text)
-                        if parsed is not None:
-                            yield parsed
-                    except json.JSONDecodeError:
-                        logging.error("Failed to parse SSE event payload as JSON")
+                # Final flush
+                parsed = flush_event()
+                if parsed is not None:
+                    yield parsed
🧹 Nitpick comments (10)
.github/workflows/component-tests.yml (7)

22-27: Strengthen pip cache hits by hashing your deps files.

Add cache-dependency-path so setup-python’s pip cache invalidates correctly when deps change.

       - name: Set up Python
         uses: actions/setup-python@v5
         with:
           python-version: '3.11'
           cache: 'pip'
+          cache-dependency-path: |
+            pyproject.toml
+            requirements*.txt

28-33: Consider installing test extras in one shot.

If you have a test extra, prefer pip install ".[test]" to ensure parity with local runs. Otherwise, this is fine.


34-67: Lowercase the input tokens to be resilient to case.

You already trim whitespace; add lowercasing so inputs like “CLI, Transport” work.

             for t in "${TOKENS[@]}"; do
               t="${t//[[:space:]]/}"
+              t="${t,,}"
               case "$t" in
                 auth) AUTH_CHANGES=true ;;

34-67: Step name is a bit misleading.

This step parses the manual selection, not “changed” files. Rename for clarity or add a separate change-detector in a PR-triggered workflow.

-      - name: Determine changed components
+      - name: Parse requested components

Is automatic changed-only selection handled in another workflow (e.g., via dorny/paths-filter on pull_request)? If not, I can add it.


75-98: Unify if: expression style for readability.

Some steps use bare expressions, others wrap with ${{ }}. Pick one style; GA docs prefer ${{ }}.

-        if: steps.changes.outputs.auth == 'true'
+        if: ${{ steps.changes.outputs.auth == 'true' }}
@@
-        if: steps.changes.outputs.cli == 'true'
+        if: ${{ steps.changes.outputs.cli == 'true' }}
@@
-        if: steps.changes.outputs.client == 'true'
+        if: ${{ steps.changes.outputs.client == 'true' }}
@@
-        if: steps.changes.outputs.fuzz_engine == 'true'
+        if: ${{ steps.changes.outputs.fuzz_engine == 'true' }}
@@
-        if: steps.changes.outputs.safety == 'true'
+        if: ${{ steps.changes.outputs.safety == 'true' }}
@@
-        if: steps.changes.outputs.transport == 'true'
+        if: ${{ steps.changes.outputs.transport == 'true' }}

113-130: Simplify Codecov upload by relying on OIDC (drop token gating).

Since you already grant id-token: write, v4 can upload without a token. This avoids secret management and still skips forks (this workflow is manual anyway).

-      - name: Check Codecov token
-        id: codecov_token
-        env:
-          CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
-        run: |
-          if [ -n "$CODECOV_TOKEN" ]; then
-            echo "has_token=true" >> $GITHUB_OUTPUT
-          else
-            echo "has_token=false" >> $GITHUB_OUTPUT
-          fi
-
       - name: Upload coverage to Codecov
-        if: ${{ (steps.changes.outputs.auth == 'true' || steps.changes.outputs.cli == 'true' || steps.changes.outputs.client == 'true' || steps.changes.outputs.fuzz_engine == 'true' || steps.changes.outputs.safety == 'true' || steps.changes.outputs.transport == 'true') && steps.coverage_check.outputs.has_coverage == 'true' && steps.codecov_token.outputs.has_token == 'true' }}
+        if: ${{ (steps.changes.outputs.auth == 'true' || steps.changes.outputs.cli == 'true' || steps.changes.outputs.client == 'true' || steps.changes.outputs.fuzz_engine == 'true' || steps.changes.outputs.safety == 'true' || steps.changes.outputs.transport == 'true') && steps.coverage_check.outputs.has_coverage == 'true' }}
         uses: codecov/codecov-action@v4
         with:
-          token: ${{ secrets.CODECOV_TOKEN }}
           files: coverage.*.xml
           fail_ci_if_error: true

15-18: Optional: add concurrency to auto-cancel superseded runs.

Prevents stacked manual runs on the same ref.

 permissions:
   contents: read
   id-token: write
   actions: read
+
+concurrency:
+  group: component-tests-${{ github.ref }}
+  cancel-in-progress: true
mcp_fuzzer/transport/sse.py (3)

34-36: Use specific exception types for policy and server errors.

Typed exceptions make it easier for callers to handle failures distinctly.

Apply:

-                raise Exception(
+                raise NetworkPolicyError(
                     "Network to non-local host is disallowed by safety policy"
                 )
@@
-                if "error" in data:
-                    raise Exception(f"Server error: {data['error']}")
+                if "error" in data:
+                    raise ServerError(f"Server error: {data['error']}")

Add near the top of this module:

class NetworkPolicyError(PermissionError):
    pass

class ServerError(RuntimeError):
    pass

Also applies to: 54-56


58-75: send_raw parses full body; consider streaming to avoid buffering entire SSE responses.

If servers keep connections open or emit large streams, this may hang or inflate memory. Optionally mirror the streaming path and return the first event.


94-102: Add return type for _stream_request and import AsyncIterator.

Improves readability and type-checking.

-from typing import Any, Dict, Optional, List
+from typing import Any, Dict, Optional, List, AsyncIterator
@@
-    async def _stream_request(self, payload: Dict[str, Any]):
+    async def _stream_request(self, payload: Dict[str, Any]) -> AsyncIterator[Dict[str, Any]]:
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7904614 and 027ce10.

📒 Files selected for processing (3)
  • .github/workflows/component-tests.yml (1 hunks)
  • mcp_fuzzer/fuzz_engine/executor.py (2 hunks)
  • mcp_fuzzer/transport/sse.py (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • mcp_fuzzer/fuzz_engine/executor.py
🧰 Additional context used
🧬 Code graph analysis (1)
mcp_fuzzer/transport/sse.py (2)
mcp_fuzzer/transport/base.py (3)
  • TransportProtocol (13-101)
  • send_raw (21-22)
  • _stream_request (53-57)
mcp_fuzzer/safety_system/policy.py (2)
  • is_host_allowed (69-112)
  • sanitize_headers (158-166)
🔇 Additional comments (4)
.github/workflows/component-tests.yml (3)

11-15: Permissions look correct for Codecov v4 (OIDC) and minimal checkout.

contents: read and id-token: write are set at workflow scope; good baseline.


99-103: LGTM on integration run and coverage target.

Runs only when any component is selected and measures the package.


104-112: Coverage presence check is fine.

Simple and reliable; no changes needed.

mcp_fuzzer/transport/sse.py (1)

23-25: SSETransport.send_request unimplemented—override get_tools/call_tool or restrict usage
send_request is correctly raising NotImplementedError (covered by tests ), but TransportProtocol.get_tools (base.py §61) and mcp_fuzzer/client/tool_client.py:55 call send_request("tools/list"). Either override get_tools (and call_tool) in sse.py to delegate to _stream_request, or ensure the factory never returns SSETransport for non-streaming flows.

@Agent-Hellboy Agent-Hellboy merged commit e741c18 into main Sep 4, 2025
6 of 8 checks passed
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.

Reorg unittest
1 participant