Skip to content

[codex] Sanitize empty optional MCP arguments#7160

Open
Tobi1chi wants to merge 4 commits intoAstrBotDevs:masterfrom
Tobi1chi:codex/sanitize-empty-mcp-arguments
Open

[codex] Sanitize empty optional MCP arguments#7160
Tobi1chi wants to merge 4 commits intoAstrBotDevs:masterfrom
Tobi1chi:codex/sanitize-empty-mcp-arguments

Conversation

@Tobi1chi
Copy link
Copy Markdown

@Tobi1chi Tobi1chi commented Mar 29, 2026

Summary

  • sanitize MCP tool arguments before dispatch so empty optional payload values are omitted
  • preserve the reconnect-and-retry path while sending cleaned arguments to the MCP session
  • add debug logging so sanitized payload changes are visible when tracing tool calls

Why

Some MCP servers reject requests when optional arguments are present but empty. This change strips empty placeholder values before the tool call so those optional fields are omitted instead of being sent as invalid payload.

Validation

  • ./.venv-precommit/bin/pre-commit run --from-ref upstream/master --to-ref HEAD

Summary by Sourcery

Sanitize MCP tool call arguments to strip empty optional values before dispatch and log any changes while preserving the reconnect-and-retry behavior.

Bug Fixes:

  • Avoid MCP tool call failures by removing empty optional arguments and nested empty collections from the payload before sending requests.

Enhancements:

  • Add debug logging to surface how MCP tool arguments are transformed during sanitization.

Tests:

  • Add unit tests for MCP argument sanitization and for falling back to empty top-level arguments in MCP tool calls.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a recursive sanitization function for MCP tool arguments to remove null values, empty strings, and empty collections. A review comment identifies an inconsistency where empty dictionaries are not handled the same as empty lists and suggests a more idiomatic implementation using comprehensions.

@Tobi1chi Tobi1chi marked this pull request as ready for review March 30, 2026 08:39
@dosubot dosubot bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Mar 30, 2026
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 3 issues, and left some high level feedback:

  • In call_tool_with_reconnect, sanitized_arguments is coerced to {} when the sanitizer returns _EMPTY_MCP_ARGUMENT, which will change the type for non-dict inputs (e.g., None or empty string); consider only defaulting to {} when the original arguments is a mapping or leaving non-mapping arguments as None/their original type after sanitization.
  • The test helper load_mcp_client_module relies on MCP_CLIENT_MODULE_PATH = REPO_ROOT / "astrbot/core/agent/mcp_client.py" and manual module construction, which is quite brittle; consider importing astrbot.core.agent.mcp_client normally and mocking its dependencies instead of using a path-based dynamic loader.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `call_tool_with_reconnect`, `sanitized_arguments` is coerced to `{}` when the sanitizer returns `_EMPTY_MCP_ARGUMENT`, which will change the type for non-dict inputs (e.g., `None` or empty string); consider only defaulting to `{}` when the original `arguments` is a mapping or leaving non-mapping arguments as `None`/their original type after sanitization.
- The test helper `load_mcp_client_module` relies on `MCP_CLIENT_MODULE_PATH = REPO_ROOT / "astrbot/core/agent/mcp_client.py"` and manual module construction, which is quite brittle; consider importing `astrbot.core.agent.mcp_client` normally and mocking its dependencies instead of using a path-based dynamic loader.

## Individual Comments

### Comment 1
<location path="astrbot/core/agent/mcp_client.py" line_range="385-390" />
<code_context>
+        sanitized_arguments = _sanitize_mcp_arguments(arguments)
+        if sanitized_arguments is _EMPTY_MCP_ARGUMENT:
+            sanitized_arguments = {}
+        if sanitized_arguments != arguments:
+            logger.debug(
+                "Sanitized MCP tool %s arguments from %s to %s",
+                tool_name,
+                arguments,
+                sanitized_arguments,
+            )
+
</code_context>
<issue_to_address>
**🚨 suggestion (security):** Debug logging of full arguments may expose sensitive data and be expensive

This logs both the raw and sanitized arguments, which can leak credentials/PII into logs and adds overhead for large payloads. Consider redacting known sensitive fields, truncating large values, or guarding this behind a more verbose/debug-only flag so it’s not enabled in typical deployments.
</issue_to_address>

### Comment 2
<location path="tests/unit/test_mcp_client.py" line_range="95-104" />
<code_context>
+    return module
+
+
+def test_sanitize_mcp_arguments_removes_nested_empty_collections():
+    mcp_client_module = load_mcp_client_module()
+
+    sanitized = mcp_client_module._sanitize_mcp_arguments(
+        {
+            "query": "hello",
+            "filters": {"tags": [], "scope": {}},
+            "metadata": {"owner": "", "visibility": None},
+        }
+    )
+
+    assert sanitized == {"query": "hello"}
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** Add more unit tests for `_sanitize_mcp_arguments` to cover top-level empties, lists, and non-empty scalar passthrough.

This test covers the nested dict/list case, but `_sanitize_mcp_arguments` has other behaviors that should be exercised:

- Top-level empties (`None`, `""`, `[]`, `{}`) returning the empty sentinel (and ultimately `{}` at the call site).
- Mixed lists (e.g. `["", None, "x", {"a": ""}]`) to verify only empty elements are removed.
- Lists of dicts where some dicts become empty and are dropped vs preserved.
- Scalar passthrough (numbers, booleans, non-empty strings) unchanged.

A small `@pytest.mark.parametrize("value, expected", [...])` test would cover these cases without much duplication.
</issue_to_address>

### Comment 3
<location path="tests/unit/test_mcp_client.py" line_range="109-118" />
<code_context>
+    assert sanitized == {"query": "hello"}
+
+
+@pytest.mark.asyncio
+async def test_call_tool_with_reconnect_falls_back_to_empty_top_level_arguments():
+    mcp_client_module = load_mcp_client_module()
+
+    client = mcp_client_module.MCPClient()
+    client.session = types.SimpleNamespace(call_tool=AsyncMock(return_value="ok"))
+
+    result = await client.call_tool_with_reconnect(
+        tool_name="search",
+        arguments={"filters": {}, "query": ""},
+        read_timeout_seconds=mcp_client_module.timedelta(seconds=1),
+    )
+
</code_context>
<issue_to_address>
**suggestion (testing):** Extend `call_tool_with_reconnect` tests to cover the retry path and ensure sanitized arguments are used on all attempts.

This test covers the empty-arguments case, but it doesn’t exercise the retry path. Please add an async test where `session.call_tool` raises `anyio.ClosedResourceError` on the first call and succeeds on the second (e.g., `side_effect=[ClosedResourceError(), "ok"]`), asserting that:

- The retry actually occurs.
- The same sanitized payload is passed on both attempts.

Optionally, also simulate `client.session` being replaced between attempts to confirm sanitized arguments are still used with the new session instance.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +385 to +390
if sanitized_arguments != arguments:
logger.debug(
"Sanitized MCP tool %s arguments from %s to %s",
tool_name,
arguments,
sanitized_arguments,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 suggestion (security): Debug logging of full arguments may expose sensitive data and be expensive

This logs both the raw and sanitized arguments, which can leak credentials/PII into logs and adds overhead for large payloads. Consider redacting known sensitive fields, truncating large values, or guarding this behind a more verbose/debug-only flag so it’s not enabled in typical deployments.

@Tobi1chi
Copy link
Copy Markdown
Author

原issue: #7116

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4200317e2b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Tobi1chi
Copy link
Copy Markdown
Author

@codex review this PR

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4200317e2b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4200317e2b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 97dc7c87ff

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +50 to +53
anyio_module = types.ModuleType("anyio")
anyio_module.ClosedResourceError = type("ClosedResourceError", (Exception,), {})
sys.modules["anyio"] = anyio_module

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Isolate sys.modules stubs to each test

load_mcp_client_module() writes fake modules into sys.modules and never restores them, so once this helper runs, later tests in the same pytest process can import these incomplete stubs (anyio, mcp, and related astrbot.* entries) instead of real modules, causing order-dependent failures or hiding real regressions. Please patch these entries via monkeypatch (or equivalent teardown) so global import state is restored after each test.

Useful? React with 👍 / 👎.

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

Labels

size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant