From cb808e7897d6173edea45ef9a8fd2124d45a79c3 Mon Sep 17 00:00:00 2001 From: Liang Wu <18244712+wuliang229@users.noreply.github.com> Date: Fri, 7 Nov 2025 23:57:05 -0800 Subject: [PATCH 1/2] Lazy import `jsonschema` The import of `jsonschema` takes up almost 2s at cold start, which dramatically slow down the cold start in a containerized environment. Changing it to lazy import within the function that uses it will save those 2s at startup. As MCP is getting used more in production, this time-saving is critical. --- src/mcp/client/session.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mcp/client/session.py b/src/mcp/client/session.py index 8f071021d..7254cef61 100644 --- a/src/mcp/client/session.py +++ b/src/mcp/client/session.py @@ -4,7 +4,6 @@ import anyio.lowlevel from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream -from jsonschema import SchemaError, ValidationError, validate from pydantic import AnyUrl, TypeAdapter from typing_extensions import deprecated @@ -376,6 +375,8 @@ async def _validate_tool_result(self, name: str, result: types.CallToolResult) - logger.warning(f"Tool {name} not listed by server, cannot validate any structured content") if output_schema is not None: + from jsonschema import SchemaError, ValidationError, validate + if result.structuredContent is None: raise RuntimeError(f"Tool {name} has an output schema but did not return structured content") try: From 3bd6dfcdafae99a18377e19bae65e0f84abc8f95 Mon Sep 17 00:00:00 2001 From: Max Isbey <224885523+maxisbey@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:18:57 +0000 Subject: [PATCH 2/2] fix: Update test mock to work with lazy jsonschema import The lazy import change caused the test's `bypass_server_output_validation()` to inadvertently mock the client's validation as well. This updates the mock to selectively bypass only server-side validation by checking the call stack. --- src/mcp/client/session.py | 2 +- tests/client/test_output_schema_validation.py | 24 ++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/mcp/client/session.py b/src/mcp/client/session.py index d205c3126..3817ca6b5 100644 --- a/src/mcp/client/session.py +++ b/src/mcp/client/session.py @@ -376,7 +376,7 @@ async def _validate_tool_result(self, name: str, result: types.CallToolResult) - if output_schema is not None: from jsonschema import SchemaError, ValidationError, validate - + if result.structuredContent is None: raise RuntimeError( f"Tool {name} has an output schema but did not return structured content" diff --git a/tests/client/test_output_schema_validation.py b/tests/client/test_output_schema_validation.py index 4e649b0eb..e4a06b7f8 100644 --- a/tests/client/test_output_schema_validation.py +++ b/tests/client/test_output_schema_validation.py @@ -19,9 +19,27 @@ def bypass_server_output_validation(): This simulates a malicious or non-compliant server that doesn't validate its outputs, allowing us to test client-side validation. """ - # Patch jsonschema.validate in the server module to disable all validation - with patch("mcp.server.lowlevel.server.jsonschema.validate"): - # The mock will simply return None (do nothing) for all validation calls + import jsonschema + + # Save the original validate function + original_validate = jsonschema.validate + + # Create a mock that tracks which module is calling it + def selective_mock(instance: Any = None, schema: Any = None, *args: Any, **kwargs: Any) -> None: + import inspect + + # Check the call stack to see where this is being called from + for frame_info in inspect.stack(): + # If called from the server module, skip validation + # TODO: fix this as it's a rather gross workaround and will eventually break + # Normalize path separators for cross-platform compatibility + normalized_path = frame_info.filename.replace("\\", "/") + if "mcp/server/lowlevel/server.py" in normalized_path: + return None + # Otherwise, use the real validation (for client-side) + return original_validate(instance=instance, schema=schema, *args, **kwargs) + + with patch("jsonschema.validate", selective_mock): yield