22
33from __future__ import annotations
44
5- from typing import Any , Awaitable , Callable
5+ from typing import Any , Awaitable , Callable , Literal
66
7- from pydantic import BaseModel , create_model
7+ from pydantic import BaseModel , Field , create_model
88
99from cubepi .agent .types import AgentTool , AgentToolResult
10- from cubepi .providers .base import Content , TextContent
10+ from cubepi .providers .base import Content , ImageContent , TextContent
1111
1212
1313def mcp_schema_to_pydantic_model (
@@ -20,22 +20,66 @@ def mcp_schema_to_pydantic_model(
2020 cubepi.AgentTool requires `parameters: type[BaseModel]`. We synthesize
2121 a model from the schema's top-level properties.
2222
23- Limited type coverage: string/integer/number/boolean/array/object.
24- Nested object schemas become dict[str, Any] (escape hatch).
23+ Type coverage: string/integer/number/boolean/array/object. Nested
24+ object schemas become dict[str, Any]. Constraint coverage on top-level
25+ properties: enum (via Literal), description, pattern, minLength,
26+ maxLength, minimum/maximum (incl. exclusive variants), minItems,
27+ maxItems. Anything else is preserved as a plain type without
28+ validation.
2529 """
2630 properties = input_schema .get ("properties" , {})
2731 required = set (input_schema .get ("required" , []))
2832
2933 fields : dict [str , Any ] = {}
3034 for prop_name , prop_schema in properties .items ():
31- py_type = _json_schema_type_to_python (prop_schema )
32- default = ... if prop_name in required else None
33- fields [prop_name ] = (py_type , default )
35+ fields [prop_name ] = _build_field (prop_schema , prop_name in required )
3436
3537 model_name = f"MCP_{ tool_name } _Input"
3638 return create_model (model_name , ** fields )
3739
3840
41+ def _build_field (prop_schema : dict [str , Any ], required : bool ) -> tuple [Any , Any ]:
42+ """Map one JSON-Schema property to a (type, FieldInfo|default) tuple."""
43+ if "enum" in prop_schema and isinstance (prop_schema ["enum" ], list ):
44+ enum_values = tuple (prop_schema ["enum" ])
45+ py_type : Any = Literal [enum_values ] if enum_values else Any
46+ else :
47+ py_type = _json_schema_type_to_python (prop_schema )
48+
49+ field_kwargs : dict [str , Any ] = {}
50+ if "description" in prop_schema :
51+ field_kwargs ["description" ] = prop_schema ["description" ]
52+
53+ # string constraints
54+ if "pattern" in prop_schema :
55+ field_kwargs ["pattern" ] = prop_schema ["pattern" ]
56+ if "minLength" in prop_schema :
57+ field_kwargs ["min_length" ] = prop_schema ["minLength" ]
58+ if "maxLength" in prop_schema :
59+ field_kwargs ["max_length" ] = prop_schema ["maxLength" ]
60+
61+ # numeric constraints
62+ if "minimum" in prop_schema :
63+ field_kwargs ["ge" ] = prop_schema ["minimum" ]
64+ if "maximum" in prop_schema :
65+ field_kwargs ["le" ] = prop_schema ["maximum" ]
66+ if "exclusiveMinimum" in prop_schema :
67+ field_kwargs ["gt" ] = prop_schema ["exclusiveMinimum" ]
68+ if "exclusiveMaximum" in prop_schema :
69+ field_kwargs ["lt" ] = prop_schema ["exclusiveMaximum" ]
70+
71+ # array constraints
72+ if "minItems" in prop_schema :
73+ field_kwargs ["min_length" ] = prop_schema ["minItems" ]
74+ if "maxItems" in prop_schema :
75+ field_kwargs ["max_length" ] = prop_schema ["maxItems" ]
76+
77+ default = ... if required else None
78+ if field_kwargs :
79+ return (py_type , Field (default = default , ** field_kwargs ))
80+ return (py_type , default )
81+
82+
3983def _json_schema_type_to_python (schema : dict [str , Any ]) -> Any :
4084 t = schema .get ("type" )
4185 if t == "string" :
@@ -91,11 +135,22 @@ async def _execute(
91135 result = await call_remote (name , args_dict )
92136 content_blocks : list [Content ] = []
93137 for c in result .get ("content" , []):
94- if c .get ("type" ) == "text" :
138+ ctype = c .get ("type" )
139+ if ctype == "text" :
95140 content_blocks .append (TextContent (text = c .get ("text" , "" )))
141+ elif ctype == "image" :
142+ content_blocks .append (
143+ ImageContent (
144+ source = c .get ("data" , "" ),
145+ media_type = c .get ("mimeType" , "" ),
146+ )
147+ )
148+ details : dict [str , Any ] = {"raw_mcp_response" : result }
149+ if "structuredContent" in result :
150+ details ["structuredContent" ] = result ["structuredContent" ]
96151 return AgentToolResult (
97152 content = content_blocks ,
98- details = { "raw_mcp_response" : result } ,
153+ details = details ,
99154 is_error = True if result .get ("isError" ) else None ,
100155 )
101156
0 commit comments