Skip to content

Commit e50bab2

Browse files
committed
fix(mcp): restore pydantic v1 compatibility for wire aliases
Use v1-safe JSON alias/runtime handling and pydantic-v1 validators in MCP wire serialization, while keeping recursive TypeAliasType behavior for v2.
1 parent 6963417 commit e50bab2

9 files changed

Lines changed: 143 additions & 83 deletions

File tree

src/dedalus_labs/lib/mcp/protocols.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,13 @@
1919
Sequence,
2020
runtime_checkable,
2121
)
22-
2322
from typing_extensions import TypeGuard
2423

25-
26-
# --- Type Aliases ------------------------------------------------------------
24+
# --- Type Aliases ---
2725

2826
MCPServerRef = str # Slug ("org/server") or URL
2927

30-
# --- Protocols ---------------------------------------------------------------
28+
# --- Protocols ---
3129

3230

3331
@runtime_checkable
@@ -99,7 +97,7 @@ def description(self) -> Optional[str]: ...
9997
def input_schema(self) -> Dict[str, Any]: ...
10098

10199

102-
# --- Helpers -----------------------------------------------------------------
100+
# --- Helpers ---
103101

104102

105103
def is_mcp_server(obj: Any) -> TypeGuard[MCPServerProtocol]:

src/dedalus_labs/lib/mcp/request.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@
1616

1717
import copy
1818
import logging
19-
from dataclasses import dataclass
2019
from typing import Any, Dict, List, Optional, Sequence
20+
from dataclasses import dataclass
2121

22-
from dedalus_labs.types.shared_params.mcp_server_spec import MCPServerSpec
2322
from dedalus_labs.types.shared_params.mcp_servers import MCPServerItem
23+
from dedalus_labs.types.shared_params.mcp_server_spec import MCPServerSpec
2424

25+
from .wire import serialize_mcp_servers
2526
from ..crypto import encrypt_credentials, fetch_encryption_key, fetch_encryption_key_sync
2627
from .protocols import CredentialProtocol
27-
from .wire import serialize_mcp_servers
2828

2929
logger = logging.getLogger(__name__)
3030

@@ -53,9 +53,7 @@ def to_dict(self) -> Dict[str, str]:
5353
return self.entries
5454

5555

56-
# ---------------------------------------------------------------------------
57-
# Request preparation
58-
# ---------------------------------------------------------------------------
56+
# --- Request Preparation ---
5957

6058

6159
async def prepare_mcp_request(
@@ -140,9 +138,7 @@ def prepare_mcp_request_sync(
140138
return data
141139

142140

143-
# ---------------------------------------------------------------------------
144-
# Internal helpers
145-
# ---------------------------------------------------------------------------
141+
# --- Internal Helpers ---
146142

147143

148144
def _encrypt_credentials(

src/dedalus_labs/lib/mcp/wire.py

Lines changed: 89 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,18 @@
1111

1212
from __future__ import annotations
1313

14-
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union, cast
15-
16-
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
14+
import re
15+
from typing import Any, Dict, List, Tuple, Union, Optional, Sequence, cast
1716
from typing_extensions import TypeAlias
1817

18+
from ... import _compat
1919
from .protocols import MCPServerProtocol, CredentialProtocol, is_mcp_server
2020

21+
if _compat.PYDANTIC_V1:
22+
from pydantic import Field, BaseModel, validator, root_validator
23+
else:
24+
from pydantic import Field, BaseModel, ConfigDict, field_validator, model_validator
25+
2126
__all__ = [
2227
# Core types
2328
"MCPServerWireSpec",
@@ -37,9 +42,7 @@
3742
]
3843

3944

40-
# ---------------------------------------------------------------------------
41-
# Type Aliases
42-
# ---------------------------------------------------------------------------
45+
# --- Type Aliases ---
4346

4447
# Serialized wire output: slug string or spec dict
4548
MCPServerWireOutput: TypeAlias = Union[str, Dict[str, Any]]
@@ -51,9 +54,7 @@
5154
ConnectionCredentialPair: TypeAlias = Tuple[Any, CredentialProtocol]
5255

5356

54-
# ---------------------------------------------------------------------------
55-
# Wire Format Model (for validation during serialization)
56-
# ---------------------------------------------------------------------------
57+
# --- Wire Format Model (for validation during serialization) ---
5758

5859

5960
class MCPServerWireSpec(BaseModel):
@@ -62,7 +63,13 @@ class MCPServerWireSpec(BaseModel):
6263
Wire format: either slug or url (not both).
6364
"""
6465

65-
model_config = ConfigDict(extra="forbid")
66+
if _compat.PYDANTIC_V1:
67+
68+
class Config:
69+
extra = "forbid"
70+
71+
else:
72+
model_config = ConfigDict(extra="forbid")
6673

6774
slug: Optional[str] = Field(
6875
default=None,
@@ -78,35 +85,78 @@ class MCPServerWireSpec(BaseModel):
7885
description="Version for MCP servers",
7986
)
8087

81-
@model_validator(mode="after")
82-
def validate_slug_or_url(self) -> MCPServerWireSpec:
83-
"""Require exactly one of slug or url."""
84-
has_slug = self.slug is not None
85-
has_url = self.url is not None
86-
87-
if not has_slug and not has_url:
88-
raise ValueError("requires either 'slug' or 'url'")
89-
if has_slug and has_url:
90-
raise ValueError("cannot have both 'slug' and 'url'")
91-
if has_slug and self.version and self.slug and "@" in self.slug:
92-
raise ValueError("cannot specify both 'version' field and version in slug")
93-
94-
return self
95-
96-
@field_validator("url")
97-
@classmethod
98-
def validate_url_format(cls, v: Optional[str]) -> Optional[str]:
99-
"""Validate URL scheme."""
100-
if v is None:
101-
return None
102-
if not v.startswith(("http://", "https://")):
103-
raise ValueError(f"URL must start with http:// or https://, got: {v}")
104-
return v
88+
if _compat.PYDANTIC_V1:
89+
90+
@root_validator(skip_on_failure=True)
91+
def validate_slug_or_url(cls, values: Dict[str, Any]) -> Dict[str, Any]:
92+
"""Require exactly one of slug or url."""
93+
slug = values.get("slug")
94+
url = values.get("url")
95+
version = values.get("version")
96+
97+
has_slug = slug is not None
98+
has_url = url is not None
99+
100+
if not has_slug and not has_url:
101+
raise ValueError("requires either 'slug' or 'url'")
102+
if has_slug and has_url:
103+
raise ValueError("cannot have both 'slug' and 'url'")
104+
if has_slug and version and isinstance(slug, str) and "@" in slug:
105+
raise ValueError("cannot specify both 'version' field and version in slug")
106+
107+
return values
108+
109+
@validator("url")
110+
def validate_url_format(cls, v: Optional[str]) -> Optional[str]:
111+
"""Validate URL scheme."""
112+
if v is None:
113+
return None
114+
if not v.startswith(("http://", "https://")):
115+
raise ValueError(f"URL must start with http:// or https://, got: {v}")
116+
return v
117+
118+
@validator("slug")
119+
def validate_slug_format(cls, v: Optional[str]) -> Optional[str]:
120+
"""Validate slug format."""
121+
if v is None:
122+
return None
123+
if not re.fullmatch(r"^[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+$", v):
124+
raise ValueError("slug must be in 'org/name' format")
125+
return v
126+
127+
else:
128+
129+
@model_validator(mode="after")
130+
def validate_slug_or_url(self) -> MCPServerWireSpec:
131+
"""Require exactly one of slug or url."""
132+
has_slug = self.slug is not None
133+
has_url = self.url is not None
134+
135+
if not has_slug and not has_url:
136+
raise ValueError("requires either 'slug' or 'url'")
137+
if has_slug and has_url:
138+
raise ValueError("cannot have both 'slug' and 'url'")
139+
if has_slug and self.version and self.slug and "@" in self.slug:
140+
raise ValueError("cannot specify both 'version' field and version in slug")
141+
142+
return self
143+
144+
@field_validator("url")
145+
@classmethod
146+
def validate_url_format(cls, v: Optional[str]) -> Optional[str]:
147+
"""Validate URL scheme."""
148+
if v is None:
149+
return None
150+
if not v.startswith(("http://", "https://")):
151+
raise ValueError(f"URL must start with http:// or https://, got: {v}")
152+
return v
105153

106154
def to_wire(self) -> MCPServerWireOutput:
107155
"""Convert to wire format. Simple slugs become strings."""
108156
if self.slug and not self.version:
109157
return self.slug
158+
if _compat.PYDANTIC_V1:
159+
return self.dict(exclude_none=True)
110160
return self.model_dump(exclude_none=True)
111161

112162
@classmethod
@@ -122,9 +172,7 @@ def from_url(cls, url: str) -> MCPServerWireSpec:
122172
return cls(url=url)
123173

124174

125-
# ---------------------------------------------------------------------------
126-
# MCP Server Serialization
127-
# ---------------------------------------------------------------------------
175+
# --- MCP Server Serialization ---
128176

129177

130178
def serialize_mcp_servers(
@@ -185,15 +233,13 @@ def _serialize_single(item: MCPServerInput) -> MCPServerWireOutput:
185233

186234
if isinstance(item, dict):
187235
# Validate and convert dict
188-
return MCPServerWireSpec.model_validate(item).to_wire()
236+
return _compat.model_parse(MCPServerWireSpec, item).to_wire()
189237

190238
# Fallback for unknown types
191239
return str(item)
192240

193241

194-
# ---------------------------------------------------------------------------
195-
# Credential Serialization
196-
# ---------------------------------------------------------------------------
242+
# --- Credential Serialization ---
197243

198244

199245
def serialize_credentials(creds: Optional[CredentialProtocol]) -> Optional[Dict[str, Any]]:
@@ -264,9 +310,7 @@ def serialize_mcp_server_with_creds(server: MCPServerProtocol) -> Dict[str, Any]
264310
return result
265311

266312

267-
# ---------------------------------------------------------------------------
268-
# Connection Serialization
269-
# ---------------------------------------------------------------------------
313+
# --- Connection Serialization ---
270314

271315

272316
def serialize_connection(connection: Any) -> Dict[str, Any]:
@@ -326,9 +370,7 @@ def collect_unique_connections(servers: Sequence[MCPServerProtocol]) -> List[Any
326370
return unique
327371

328372

329-
# ---------------------------------------------------------------------------
330-
# Credential Matching
331-
# ---------------------------------------------------------------------------
373+
# --- Credential Matching ---
332374

333375

334376
def match_credentials_to_server(

src/dedalus_labs/lib/runner/protocols.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,13 @@
1919
Sequence,
2020
runtime_checkable,
2121
)
22-
2322
from typing_extensions import TypeGuard
2423

25-
26-
# --- Type Aliases ------------------------------------------------------------
24+
# --- Type Aliases ---
2725

2826
MCPServerRef = str # Slug ("org/server") or URL
2927

30-
# --- Protocols ---------------------------------------------------------------
28+
# --- Protocols ---
3129

3230

3331
@runtime_checkable
@@ -92,7 +90,7 @@ def description(self) -> Optional[str]: ...
9290
def input_schema(self) -> Dict[str, Any]: ...
9391

9492

95-
# --- Helpers -----------------------------------------------------------------
93+
# --- Helpers ---
9694

9795

9896
def is_mcp_server(obj: Any) -> TypeGuard[MCPServerProtocol]:

src/dedalus_labs/types/shared/json_object_input.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@
22

33
from __future__ import annotations
44

5-
from typing import Dict, Optional
5+
from typing import Dict
66
from typing_extensions import TypeAliasType
77

8+
from ... import _compat
9+
810
__all__ = ["JSONObjectInput"]
911

1012
from .json_value_input import JSONValueInput
1113

12-
JSONObjectInput = TypeAliasType("JSONObjectInput", Dict[str, Optional[JSONValueInput]])
14+
if _compat.PYDANTIC_V1:
15+
JSONObjectInput = Dict[str, JSONValueInput]
16+
else:
17+
JSONObjectInput = TypeAliasType("JSONObjectInput", Dict[str, JSONValueInput])

src/dedalus_labs/types/shared/json_value_input.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@
22

33
from __future__ import annotations
44

5-
from typing import Dict, List, Union, Optional
5+
from typing import Dict, List, Union
66
from typing_extensions import TypeAliasType
77

8+
from ... import _compat
9+
810
__all__ = ["JSONValueInput"]
911

10-
JSONValueInput = TypeAliasType(
11-
"JSONValueInput",
12-
Union[str, float, bool, Dict[str, Optional["JSONValueInput"]], List[Optional["JSONValueInput"]], None],
13-
)
12+
if _compat.PYDANTIC_V1:
13+
# Pydantic v1 does not support recursive TypeAliasType.
14+
JSONValueInput = Union[str, float, bool, Dict[str, object], List[object], None]
15+
else:
16+
JSONValueInput = TypeAliasType(
17+
"JSONValueInput",
18+
Union[str, float, bool, Dict[str, "JSONValueInput"], List["JSONValueInput"], None],
19+
)

src/dedalus_labs/types/shared_params/json_object_input.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@
22

33
from __future__ import annotations
44

5-
from typing import Dict, Optional
5+
from typing import Dict
66
from typing_extensions import TypeAliasType
77

8+
from ... import _compat
9+
810
__all__ = ["JSONObjectInput"]
911

1012
from .json_value_input import JSONValueInput
1113

12-
JSONObjectInput = TypeAliasType("JSONObjectInput", Dict[str, Optional[JSONValueInput]])
14+
if _compat.PYDANTIC_V1:
15+
JSONObjectInput = Dict[str, JSONValueInput]
16+
else:
17+
JSONObjectInput = TypeAliasType("JSONObjectInput", Dict[str, JSONValueInput])

src/dedalus_labs/types/shared_params/json_value_input.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@
22

33
from __future__ import annotations
44

5-
from typing import Dict, Union, Optional
5+
from typing import Dict, Union
66
from typing_extensions import TypeAliasType
77

8+
from ... import _compat
89
from ..._types import SequenceNotStr
910

1011
__all__ = ["JSONValueInput"]
1112

12-
JSONValueInput = TypeAliasType(
13-
"JSONValueInput",
14-
Union[str, float, bool, Dict[str, Optional["JSONValueInput"]], SequenceNotStr[Optional["JSONValueInput"]]],
15-
)
13+
if _compat.PYDANTIC_V1:
14+
# Pydantic v1 does not support recursive TypeAliasType.
15+
JSONValueInput = Union[str, float, bool, Dict[str, object], SequenceNotStr[object]]
16+
else:
17+
JSONValueInput = TypeAliasType(
18+
"JSONValueInput",
19+
Union[str, float, bool, Dict[str, "JSONValueInput"], SequenceNotStr["JSONValueInput"]],
20+
)

0 commit comments

Comments
 (0)