diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..bdbeaa6 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index 6e54aea..763ecfe 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,7 @@ wheels/ notebooks/ # mkdocs documentation -/site \ No newline at end of file +/site + +# macOS files +.DS_Store \ No newline at end of file diff --git a/src/mcpadapt/crewai_adapter.py b/src/mcpadapt/crewai_adapter.py index 3b9cde4..9706f64 100644 --- a/src/mcpadapt/crewai_adapter.py +++ b/src/mcpadapt/crewai_adapter.py @@ -15,7 +15,10 @@ from pydantic import BaseModel from mcpadapt.core import ToolAdapter -from mcpadapt.utils.modeling import create_model_from_json_schema +from mcpadapt.utils.modeling import ( + create_model_from_json_schema, + resolve_refs_and_remove_defs +) json_type_mapping: dict[str, Type] = { "string": str, @@ -51,6 +54,9 @@ def adapt( Returns: A CrewAI tool. """ + mcp_tool.inputSchema = resolve_refs_and_remove_defs( + mcp_tool.inputSchema + ) ToolInput = create_model_from_json_schema(mcp_tool.inputSchema) class CrewAIMCPTool(BaseTool): diff --git a/src/mcpadapt/utils/modeling.py b/src/mcpadapt/utils/modeling.py index 8464f68..af15896 100644 --- a/src/mcpadapt/utils/modeling.py +++ b/src/mcpadapt/utils/modeling.py @@ -1,6 +1,7 @@ +from copy import deepcopy from typing import Any, Dict, ForwardRef, List, Optional, Type, Union - from pydantic import BaseModel, Field, create_model +import re json_type_mapping: dict[str, Type] = { "string": str, @@ -21,6 +22,33 @@ } +def resolve_refs_and_remove_defs(json_obj): + # Extract $defs + defs = json_obj.get("$defs", {}) + + # Function to recursively resolve $ref + def _resolve(obj): + if isinstance(obj, dict): + if "$ref" in obj: + ref_path = obj["$ref"] + match = re.match(r"#/\$defs/(\w+)", ref_path) + if match: + def_key = match.group(1) + return _resolve(deepcopy(defs.get(def_key, {}))) + return {k: _resolve(v) for k, v in obj.items()} + elif isinstance(obj, list): + return [_resolve(i) for i in obj] + else: + return obj + + json_obj = _resolve(json_obj) + + # Remove $defs + json_obj.pop("$defs", None) + + return json_obj + + def create_model_from_json_schema( schema: dict[str, Any], model_name: str = "DynamicModel" ) -> Type[BaseModel]: @@ -51,6 +79,10 @@ def process_schema(name: str, schema_def: Dict[str, Any]) -> Type[BaseModel]: default=default, description=field_schema.get("description", ""), title=field_schema.get("title", ""), + items=field_schema.get("items", None), + anyOf=field_schema.get("anyOf", []), + enum=field_schema.get("enum", None), + properties=field_schema.get("properties", {}), ), ) diff --git a/tests/test_crewai_adapter.py b/tests/test_crewai_adapter.py index f8308e3..da226bd 100644 --- a/tests/test_crewai_adapter.py +++ b/tests/test_crewai_adapter.py @@ -1,3 +1,6 @@ +import ast +import re + from textwrap import dedent import pytest @@ -7,6 +10,22 @@ from mcpadapt.crewai_adapter import CrewAIAdapter +def extract_and_eval_dict(text): + # Match the first outermost curly brace block + match = re.search(r'\{.*\}', text, re.DOTALL) + if not match: + raise ValueError("No dictionary-like structure found in the string.") + + dict_str = match.group(0) + + try: + # Safer than eval for parsing literals + parsed_dict = ast.literal_eval(dict_str) + return parsed_dict + except Exception as e: + raise ValueError(f"Failed to evaluate dictionary: {e}") + + @pytest.fixture def echo_server_script(): return dedent( @@ -25,6 +44,60 @@ def echo_tool(text: str) -> str: ) +@pytest.fixture +def custom_script_with_custom_arguments(): + return dedent( + ''' + from mcp.server.fastmcp import FastMCP + from typing import Literal + from enum import Enum + from pydantic import BaseModel + + class Animal(BaseModel): + legs: int + name: str + + mcp = FastMCP("Server") + + @mcp.tool() + def custom_tool( + text: Literal["ciao", "hello"], + animal: Animal, + env: str | None = None, + + ) -> str: + pass + + mcp.run() + ''' + ) + + +@pytest.fixture +def custom_script_with_custom_list(): + return dedent( + ''' + from mcp.server.fastmcp import FastMCP + from pydantic import BaseModel + + class Point(BaseModel): + x: float + y: float + + mcp = FastMCP("Server") + + @mcp.tool() + def custom_tool( + points: list[Point], + + ) -> str: + pass + + mcp.run() + ''' + ) + + @pytest.fixture def echo_server_sse_script(): return dedent( @@ -108,6 +181,64 @@ def test_basic_sync(echo_server_script): assert tools[0].run(text="hello") == "Echo: hello" +# Fails if enums, unions, or pydantic classes are not included in the +# generated schema +def test_basic_sync_custom_arguments(custom_script_with_custom_arguments): + with MCPAdapt( + StdioServerParameters( + command="uv", + args=[ + "run", + "python", + "-c", + custom_script_with_custom_arguments + ] + ), + CrewAIAdapter(), + ) as tools: + tools_dict = extract_and_eval_dict(tools[0].description) + assert tools_dict != {} + assert tools_dict["properties"] != {} + # Enum tests + assert "enum" in tools_dict["properties"]["text"] + assert "hello" in tools_dict["properties"]["text"]["enum"] + assert "ciao" in tools_dict["properties"]["text"]["enum"] + # Pydantic class tests + assert tools_dict["properties"]["animal"]["properties"] != {} + assert tools_dict["properties"]["animal"]["properties"]["legs"] != {} + assert tools_dict["properties"]["animal"]["properties"]["name"] != {} + # Union tests + assert "anyOf" in tools_dict["properties"]["env"] + assert tools_dict["properties"]["env"]["anyOf"] != [] + types = [ + opt.get("type") for opt in tools_dict["properties"]["env"]["anyOf"] + ] + assert "null" in types + assert "string" in types + +# Raises KeyError +# if the pydantic objects list is not correctly resolved with $ref handling +# within mcp_tool.inputSchema +def test_basic_sync_custom_list(custom_script_with_custom_list): + with MCPAdapt( + StdioServerParameters( + command="uv", + args=[ + "run", + "python", + "-c", + custom_script_with_custom_list + ] + ), + CrewAIAdapter(), + ) as tools: + tools_dict = extract_and_eval_dict(tools[0].description) + assert tools_dict != {} + assert tools_dict["properties"] != {} + # Pydantic class tests + assert tools_dict["properties"]["points"]["items"] != {} + + def test_basic_sync_sse(echo_sse_server): sse_serverparams = echo_sse_server with MCPAdapt( diff --git a/uv.lock b/uv.lock index 804d1ef..64c6b57 100644 --- a/uv.lock +++ b/uv.lock @@ -889,30 +889,16 @@ wheels = [ [[package]] name = "duckduckgo-search" -version = "2025.4.4" +version = "8.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "lxml" }, { name = "primp" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/66/1f/8a66088ae1d7a68f40db9890642435cfff2b54701f47f19d5cc9404d5f65/duckduckgo_search-8.0.1.tar.gz", hash = "sha256:1d40d4425062a33dc72d19603e25ebf36b212bd8ef0662bff39fd47598226f6f", size = 21932 } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/a1/02fad7df6feb8c1ac1ec640c32275f5cdd02fe322b9bd5182fe1b7a72716/duckduckgo_search-2025.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ad6e748b16974d6eb95b3604ecad7a6c85337e34f7b7e0ec5ef8ba4b1f8b8e1", size = 78988 }, - { url = "https://files.pythonhosted.org/packages/55/e6/a62ad4522e46a1dcf202e13fbd6ab585f0022f753c556738bdfeb36ab16e/duckduckgo_search-2025.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:85244f9791da37bbaf250f341c490db0005ca16f823bd75dd4734f6a833a48b1", size = 76705 }, - { url = "https://files.pythonhosted.org/packages/b6/30/5527c1f3536c6b2678f18b4497b77f71bf2f9fca2d61e4382d76052cd641/duckduckgo_search-2025.4.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9dfba8734f8f6220d63777efaa7a70a180e77b91d414736e2b3c862041f8978", size = 96565 }, - { url = "https://files.pythonhosted.org/packages/3e/89/73ccd75fbd54392d060b9b575dbef9614aaf42303f6b78b236575655f212/duckduckgo_search-2025.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:e63e374508573813caf82694686d5f5aca5c28d97c51a4ce14acba1ced7b3ba1", size = 60503 }, - { url = "https://files.pythonhosted.org/packages/2a/a0/d16e8573788ff073c57c02d065b21218a3a3b4c8f4193c3d6bfd3ba18c1b/duckduckgo_search-2025.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c93136df7ab2356a2c72e4a5b12468b4211001966eaa0f7e916fec21c6a030a8", size = 77705 }, - { url = "https://files.pythonhosted.org/packages/b8/1e/c5080229a0af6e521c4b96bc322521b6653ec78b1ce5a9e88ff32bea2be3/duckduckgo_search-2025.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cba3328e3b445108eea2885a067d7940df9d4ac4a80896bf8cd0d20bb775b33a", size = 75141 }, - { url = "https://files.pythonhosted.org/packages/5c/49/0cd13c7c74c1ff627a390127ed66a133a5a96fc98c20537996ced261f307/duckduckgo_search-2025.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:343a30ec5c8be287aad44e657e5ece2e10ef319642feac346ffe485704363a7c", size = 94924 }, - { url = "https://files.pythonhosted.org/packages/c2/e9/f4233afcca57a239cc605171ec7b1ce0540088e48aa6e7bbbc00ecffe03d/duckduckgo_search-2025.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ce7de2a211b012b2e761f937efe77151010cf97075262efe08811ed987fe92a", size = 60343 }, - { url = "https://files.pythonhosted.org/packages/77/15/bea5add03f5028dba529ddd9090ec6bd545e90926bdaa5a80b122e265110/duckduckgo_search-2025.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:18f21c972613f96fba344e2186757e862b724d9e574c956193ebc4f7fdfdf0fa", size = 78319 }, - { url = "https://files.pythonhosted.org/packages/4a/ee/6ce5a5db585af44e4a811d0fd61431fe13789e51ade1102e78dd4f610a55/duckduckgo_search-2025.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8c6944fb74b77aaab5759c8806c23ed20aa96624d1c49a2caf2b9f3b50c07057", size = 75032 }, - { url = "https://files.pythonhosted.org/packages/e2/2f/a899d28de2d46541b24d33b969fedda74f6b00a4317e16cf207cbb896292/duckduckgo_search-2025.4.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e52028aeaa6151514a3115cc6fe616883cf9614fba2966792a415b7d97935b6", size = 95875 }, - { url = "https://files.pythonhosted.org/packages/58/f3/cf71d8a286325f8a2d969bc0bda9e4defb01f1df2142887c965fa03adebb/duckduckgo_search-2025.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:f8a826f0d19eccba76d83d5034be8ae2e89f38263befd290c6ca613c8eed7ea2", size = 60488 }, - { url = "https://files.pythonhosted.org/packages/85/d6/8d8775727dc18a38b5bbd5982383ff4d2a1b392afba4a998cac54e5c78f8/duckduckgo_search-2025.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a34c01f397f843ea9e4d00520eadfee087f837c3be9d7c2817c6006ec634c057", size = 78209 }, - { url = "https://files.pythonhosted.org/packages/97/72/e1d4db7ef67fe35fe996ad492b253cdfba79652d5ef03c1e0159b6d1a05e/duckduckgo_search-2025.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7fc5625842c14b0f80b955f448757976295f4242ade632b6982b5ae002fe3d81", size = 74936 }, - { url = "https://files.pythonhosted.org/packages/0b/7c/dc478eea16f30270183a2d5a964352404c00b3fcc5df8ce64a1220a5a556/duckduckgo_search-2025.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5434abee02b3d0c9759210a7d358aaa51214c1529ab1e6ddf374d94afa2a0e5e", size = 95537 }, - { url = "https://files.pythonhosted.org/packages/75/eb/a16b1e6516c0156c04499367eb7b7bcaec1b69e91a25d449df2ee97f1377/duckduckgo_search-2025.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:a2cf0c92deb868eeb0694d864aa922e054e3a87da81009757af6af56d700f681", size = 60237 }, + { url = "https://files.pythonhosted.org/packages/83/a2/66adca41164860dee6d2d47b506fef3262c8879aab727b687c798d67313f/duckduckgo_search-8.0.1-py3-none-any.whl", hash = "sha256:87ea18d9abb1cd5dc8f63fc70ac867996acce2cb5e0129d191b9491c202420be", size = 18125 }, ] [[package]] @@ -3776,18 +3762,18 @@ wheels = [ [[package]] name = "primp" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/1e/a063129aed2320b463fd35c5d918d5754e59011698aaf7cf297a610b3380/primp-0.14.0.tar.gz", hash = "sha256:b6f23b2b694118a9d0443b3760698b90afb6f867f8447e71972530f48297992e", size = 112406 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/12/eba13ddbeb5c6df6cf7511aedb5fa4bcb99c0754e88056260dd44aa53929/primp-0.14.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bd2dfb57feeba21a77a1128b6c6f17856605c4e73edcc05764fb134de4ff014f", size = 3173837 }, - { url = "https://files.pythonhosted.org/packages/77/65/3cd25b4f4d0cd9de4f1d95858dcddd7ed082587524294c179c847de18951/primp-0.14.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:31eecb5316f9bd732a7994530b85eb698bf6500d2f6c5c3382dac0353f77084e", size = 2947192 }, - { url = "https://files.pythonhosted.org/packages/13/77/f85bc3e31befa9b9bac54bab61beb34ff84a70d20f02b7dcd8abc120120a/primp-0.14.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11229e65aa5755fdfb535cc03fd64259a06764ad7c22e650fb3bea51400f1d09", size = 3276730 }, - { url = "https://files.pythonhosted.org/packages/44/36/bc95049264ee668a5cdaadf77ef711aaa9cb0c4c0a246b27bba9a2f0114c/primp-0.14.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8f56ca2cd63f9ac75b33bf48129b7e79ade29cf280bc253b17b052afb27d2b9e", size = 3247684 }, - { url = "https://files.pythonhosted.org/packages/31/d9/632a70c80dcdd0bb9293cdc7e7543d35e5912325631c3e9f3b7c7d842941/primp-0.14.0-cp38-abi3-manylinux_2_34_armv7l.whl", hash = "sha256:3fb204f67a4b58dc53f3452143121317b474437812662ac0149d332a77ecbe1a", size = 3007835 }, - { url = "https://files.pythonhosted.org/packages/dc/ba/07b04b9d404f20ec78449c5974c988a5adf7d4d245a605466486f70d35c3/primp-0.14.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0b21e6a599f580137774623009c7f895afab49d6c3d6c9a28344fd2586ebe8a", size = 3413956 }, - { url = "https://files.pythonhosted.org/packages/d7/d3/3bee499b4594fce1f8ccede785e517162407fbea1d452c4fb55fe3fb5e81/primp-0.14.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6549766ece3c7be19e1c16fa9029d3e50fa73628149d88601fcd964af8b44a8d", size = 3595850 }, - { url = "https://files.pythonhosted.org/packages/6a/20/042c8ae21d185f2efe61780dfbc01464c982f59626b746d5436c2e4c1e08/primp-0.14.0-cp38-abi3-win_amd64.whl", hash = "sha256:d3ae1ba954ec8d07abb527ccce7bb36633525c86496950ba0178e44a0ea5c891", size = 3143077 }, +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/0b/a87556189da4de1fc6360ca1aa05e8335509633f836cdd06dd17f0743300/primp-0.15.0.tar.gz", hash = "sha256:1af8ea4b15f57571ff7fc5e282a82c5eb69bc695e19b8ddeeda324397965b30a", size = 113022 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/5a/146ac964b99ea7657ad67eb66f770be6577dfe9200cb28f9a95baffd6c3f/primp-0.15.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:1b281f4ca41a0c6612d4c6e68b96e28acfe786d226a427cd944baa8d7acd644f", size = 3178914 }, + { url = "https://files.pythonhosted.org/packages/bc/8a/cc2321e32db3ce64d6e32950d5bcbea01861db97bfb20b5394affc45b387/primp-0.15.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:489cbab55cd793ceb8f90bb7423c6ea64ebb53208ffcf7a044138e3c66d77299", size = 2955079 }, + { url = "https://files.pythonhosted.org/packages/c3/7b/cbd5d999a07ff2a21465975d4eb477ae6f69765e8fe8c9087dab250180d8/primp-0.15.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c18b45c23f94016215f62d2334552224236217aaeb716871ce0e4dcfa08eb161", size = 3281018 }, + { url = "https://files.pythonhosted.org/packages/1b/6e/a6221c612e61303aec2bcac3f0a02e8b67aee8c0db7bdc174aeb8010f975/primp-0.15.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e985a9cba2e3f96a323722e5440aa9eccaac3178e74b884778e926b5249df080", size = 3255229 }, + { url = "https://files.pythonhosted.org/packages/3b/54/bfeef5aca613dc660a69d0760a26c6b8747d8fdb5a7f20cb2cee53c9862f/primp-0.15.0-cp38-abi3-manylinux_2_34_armv7l.whl", hash = "sha256:6b84a6ffa083e34668ff0037221d399c24d939b5629cd38223af860de9e17a83", size = 3014522 }, + { url = "https://files.pythonhosted.org/packages/ac/96/84078e09f16a1dad208f2fe0f8a81be2cf36e024675b0f9eec0c2f6e2182/primp-0.15.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:592f6079646bdf5abbbfc3b0a28dac8de943f8907a250ce09398cda5eaebd260", size = 3418567 }, + { url = "https://files.pythonhosted.org/packages/6c/80/8a7a9587d3eb85be3d0b64319f2f690c90eb7953e3f73a9ddd9e46c8dc42/primp-0.15.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5a728e5a05f37db6189eb413d22c78bd143fa59dd6a8a26dacd43332b3971fe8", size = 3606279 }, + { url = "https://files.pythonhosted.org/packages/0c/dd/f0183ed0145e58cf9d286c1b2c14f63ccee987a4ff79ac85acc31b5d86bd/primp-0.15.0-cp38-abi3-win_amd64.whl", hash = "sha256:aeb6bd20b06dfc92cfe4436939c18de88a58c640752cf7f30d9e4ae893cdec32", size = 3149967 }, ] [[package]]