Commit 4a22391
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
2 files changed
+5
-2
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| |||
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
| 26 | + | |
26 | 27 | | |
27 | 28 | | |
28 | 29 | | |
| |||
38 | 39 | | |
39 | 40 | | |
40 | 41 | | |
| 42 | + | |
41 | 43 | | |
42 | 44 | | |
43 | 45 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
13 | | - | |
| 13 | + | |
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| |||
94 | 94 | | |
95 | 95 | | |
96 | 96 | | |
| 97 | + | |
97 | 98 | | |
98 | 99 | | |
99 | 100 | | |
| |||
0 commit comments