1111
1212from __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
1716from typing_extensions import TypeAlias
1817
18+ from ... import _compat
1919from .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" ,
3742]
3843
3944
40- # ---------------------------------------------------------------------------
41- # Type Aliases
42- # ---------------------------------------------------------------------------
45+ # --- Type Aliases ---
4346
4447# Serialized wire output: slug string or spec dict
4548MCPServerWireOutput : TypeAlias = Union [str , Dict [str , Any ]]
5154ConnectionCredentialPair : 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
5960class 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
130178def 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
199245def 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
272316def 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
334376def match_credentials_to_server (
0 commit comments