Skip to content

Commit 4a22391

Browse files
awg66alex-gatto-wd
andauthored
Fix protocol schemas so that they work with pydantic (#361)
**Summary** This PR adds the @typing.runtime_checkable decorator to the following protocols: - LoggingMessageCallback - ProgressCallback - ToolCallInterceptor **Motivation** When these protocols are used as type hints within Pydantic V2 models (specifically when arbitrary_types_allowed=True is set), Pydantic attempts to automatically generate an isinstance validator for the field. Because these protocols were not marked as runtime checkable, standard Python isinstance() checks fail, causing Pydantic to raise a SchemaError during model creation. Adding @runtime_checkable allows isinstance() to work correctly with these protocols, enabling downstream users to include standard LangChain MCP callbacks and interceptors directly in their Pydantic models without needing to type them as Any. **Error Reference** Without this change, attempting to use these types in a Pydantic V2 model results in tracebacks similar to: ``` E pydantic_core._pydantic_core.SchemaError: Error building "is-instance" validator: E SchemaError: 'cls' must be valid as the first argument to 'isinstance' ``` Test Plan [x] Verified that Pydantic V2 models can now successfully use these types as fields without raising SchemaError. ``` (langchain-mcp-adapters) ➜ langchain-mcp-adapters git:(fix_protocol_schemas_pydantic) ✗ uv run ipython Python 3.12.10 (main, May 17 2025, 13:40:56) [Clang 20.1.4 ] Type 'copyright', 'credits' or 'license' for more information IPython 9.7.0 -- An enhanced Interactive Python. Type '?' for help. Tip: Use the IPython.lib.demo.Demo class to load any Python script as an interactive demo. In [1]: from pydantic import BaseModel ...: from langchain_mcp_adapters.callbacks import LoggingMessageCallback, ProgressCallback ...: from langchain_mcp_adapters.interceptors import ToolCallInterceptor ...: ...: print("Attempting to build Pydantic model with Protocols...") ...: ...: try: ...: # Define a model that uses the problematic types ...: class TestModel(BaseModel, arbitrary_types_allowed=True): ...: logging_cb: LoggingMessageCallback | None = None ...: progress_cb: ProgressCallback | None = None ...: interceptor: ToolCallInterceptor | None = None ...: ...: # Instantiate it to trigger standard schema validation ...: model = TestModel() ...: print("\n✅ SUCCESS: Pydantic model built successfully! The fix is working.") ...: ...: # Double check runtime_checkable is actually working ...: print("Testing isinstance checks (should be True now)...") ...: print(f"LoggingMessageCallback is runtime checkable: {isinstance(LoggingMessageCallback, type)}") ...: ...: except Exception as e: ...: print(f"\n❌ FAILED: Still getting error:\n{e}") ...: Attempting to build Pydantic model with Protocols... ✅ SUCCESS: Pydantic model built successfully! The fix is working. Testing isinstance checks (should be True now)... LoggingMessageCallback is runtime checkable: True ``` Co-authored-by: alex.gatto <[email protected]>
1 parent 8b02c53 commit 4a22391

File tree

2 files changed

+5
-2
lines changed

2 files changed

+5
-2
lines changed

langchain_mcp_adapters/callbacks.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Types for callbacks."""
22

33
from dataclasses import dataclass
4-
from typing import Protocol
4+
from typing import Protocol, runtime_checkable
55

66
from mcp.client.session import LoggingFnT as MCPLoggingFnT
77
from mcp.shared.session import ProgressFnT as MCPProgressFnT
@@ -23,6 +23,7 @@ class CallbackContext:
2323
tool_name: str | None = None
2424

2525

26+
@runtime_checkable
2627
class LoggingMessageCallback(Protocol):
2728
"""Light wrapper around the mcp.client.session.LoggingFnT.
2829
@@ -38,6 +39,7 @@ async def __call__(
3839
...
3940

4041

42+
@runtime_checkable
4143
class ProgressCallback(Protocol):
4244
"""Light wrapper around the mcp.shared.session.ProgressFnT.
4345

langchain_mcp_adapters/interceptors.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from __future__ import annotations
1111

1212
from dataclasses import dataclass, replace
13-
from typing import TYPE_CHECKING, Any, Protocol
13+
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
1414

1515
from mcp.types import CallToolResult
1616
from typing_extensions import NotRequired, TypedDict, Unpack
@@ -94,6 +94,7 @@ def override(
9494
return replace(self, **overrides)
9595

9696

97+
@runtime_checkable
9798
class ToolCallInterceptor(Protocol):
9899
"""Protocol for tool call interceptors using handler callback pattern.
99100

0 commit comments

Comments
 (0)