diff --git a/docs/user_guide/how_to_guides/mcp.md b/docs/user_guide/how_to_guides/mcp.md index 416a59db..aeb86853 100644 --- a/docs/user_guide/how_to_guides/mcp.md +++ b/docs/user_guide/how_to_guides/mcp.md @@ -27,6 +27,8 @@ Install the MCP extra: pip install redisvl[mcp] ``` +The extra installs FastMCP 2.13.0 or later, below 4. If you pin FastMCP yourself, stay within that range, because the server does not work with older releases. + If your vectorizer needs a provider extra, install that too: ```bash diff --git a/pyproject.toml b/pyproject.toml index cd19faa8..4e6a7ce5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,11 @@ mcp = [ # stateless redesign of the protocol core, so a major bump could change both # the negotiated version and how tools advertise their input schemas. # Supporting that revision is its own piece of work. - "fastmcp>=2.0.0,<4", + # The floor is the oldest release RedisVL works with, not a security floor: + # earlier 2.x releases lack fastmcp.server.auth, fail to import with + # pydantic 2.13, reject the base_url the JWT verifier is built with, or + # (2.12.1 to 2.12.5) leave SSE clients hanging while they list tools. + "fastmcp>=2.13.0,<4", "pydantic-settings>=2.0,<3", ] mistralai = ["mistralai>=1.0.0,<2"] diff --git a/tests/integration/test_mcp/test_profiles.py b/tests/integration/test_mcp/test_profiles.py index bf01b4cf..f2572662 100644 --- a/tests/integration/test_mcp/test_profiles.py +++ b/tests/integration/test_mcp/test_profiles.py @@ -1,3 +1,4 @@ +from importlib.metadata import version from pathlib import Path import pytest @@ -258,10 +259,13 @@ async def test_profile_advertises_only_its_exposed_arguments(started_server): # a fake, since the schema is derived by the library from the signature. assert set(schema["properties"]) == {"query", "limit", "filter"} # A set, not a list: `required` ordering is a pydantic implementation detail, - # and only its membership is behavior. `fastmcp` is pinned `>=2.0.0` with no - # upper bound, so a minor bump must not break this on presentation alone. + # and only its membership is behavior. `fastmcp` is allowed anywhere in + # `>=2.13.0,<4`, so a minor bump must not break this on presentation alone. assert set(schema["required"]) == {"query"} - assert schema["additionalProperties"] is False + # FastMCP 2.x leaves `additionalProperties` out of the schema but still + # refuses extra arguments at call time; only 3.x publishes the refusal. + if int(version("fastmcp").split(".")[0]) >= 3: + assert schema["additionalProperties"] is False # A raw string filter has no safe composition with a locked filter, so the # object form is the only shape offered. Optional[dict] is encoded as an diff --git a/tests/unit/test_mcp/test_profiles_unit.py b/tests/unit/test_mcp/test_profiles_unit.py index bf941747..00a798f2 100644 --- a/tests/unit/test_mcp/test_profiles_unit.py +++ b/tests/unit/test_mcp/test_profiles_unit.py @@ -1043,7 +1043,9 @@ async def record_search(server, **kwargs): async with fastmcp.Client(mcp) as client: (tool,) = await client.list_tools() assert "category" not in tool.inputSchema["properties"] - assert tool.inputSchema["additionalProperties"] is False + # FastMCP 2.x omits the key; the refused call below holds either way. + if int(fastmcp.__version__.split(".")[0]) >= 3: + assert tool.inputSchema["additionalProperties"] is False refused = await client.call_tool( tool.name, {"query": "jam", "category": "victim"}, raise_on_error=False diff --git a/uv.lock b/uv.lock index 3e2cecd6..788bc820 100644 --- a/uv.lock +++ b/uv.lock @@ -4956,7 +4956,7 @@ requires-dist = [ { name = "boto3", marker = "extra == 'bedrock'", specifier = ">=1.36.0,<2" }, { name = "cohere", marker = "extra == 'all'", specifier = ">=4.44" }, { name = "cohere", marker = "extra == 'cohere'", specifier = ">=4.44" }, - { name = "fastmcp", marker = "extra == 'mcp'", specifier = ">=2.0.0,<4" }, + { name = "fastmcp", marker = "extra == 'mcp'", specifier = ">=2.13.0,<4" }, { name = "google-cloud-aiplatform", marker = "extra == 'all'", specifier = ">=1.26,<2.0.0" }, { name = "google-cloud-aiplatform", marker = "extra == 'vertexai'", specifier = ">=1.26,<2.0.0" }, { name = "google-genai", marker = "extra == 'all'", specifier = ">=1.0.0" },