From c46c6852c6b2fbb2a893462fd2581f9bf4de2642 Mon Sep 17 00:00:00 2001 From: Vishal Bala Date: Fri, 30 Oct 2026 15:15:11 +0100 Subject: [PATCH 1/3] fix(mcp): raise the fastmcp floor to the oldest release that works The mcp extra declared fastmcp>=2.0.0, but RedisVL's MCP server does not work below 2.12.1. 2.0 to 2.3 lack fastmcp.server.auth and fastmcp.server.dependencies, 2.4 to 2.11.1 fail to import with pydantic 2.13, and 2.11.2 to 2.12.0 reject the base_url the JWT verifier is built with. The MCP unit tests pass on 2.12.1 through 3.4.11 apart from tests that patch FastMCP 3.1+ settings. The floor is functional, not a security floor, so users can still choose any working release. --- docs/user_guide/how_to_guides/mcp.md | 2 ++ pyproject.toml | 5 ++++- uv.lock | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/user_guide/how_to_guides/mcp.md b/docs/user_guide/how_to_guides/mcp.md index 416a59db..0fecdeba 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.12.1 or later, below 4. If you pin FastMCP yourself, stay within that range: older releases are missing APIs the server depends on. + If your vectorizer needs a provider extra, install that too: ```bash diff --git a/pyproject.toml b/pyproject.toml index cd19faa8..e3a5e5a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,10 @@ 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, or reject the base_url the JWT verifier is built with. + "fastmcp>=2.12.1,<4", "pydantic-settings>=2.0,<3", ] mistralai = ["mistralai>=1.0.0,<2"] diff --git a/uv.lock b/uv.lock index 3e2cecd6..90818cb1 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.12.1,<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" }, From 390b1381de7c181281f2ff0a182cad8be2b03ec9 Mon Sep 17 00:00:00 2001 From: Vishal Bala Date: Fri, 30 Oct 2026 15:16:07 +0100 Subject: [PATCH 2/3] docs(mcp): word the FastMCP range note accurately and fix a stale test comment --- docs/user_guide/how_to_guides/mcp.md | 2 +- tests/integration/test_mcp/test_profiles.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/user_guide/how_to_guides/mcp.md b/docs/user_guide/how_to_guides/mcp.md index 0fecdeba..03d51493 100644 --- a/docs/user_guide/how_to_guides/mcp.md +++ b/docs/user_guide/how_to_guides/mcp.md @@ -27,7 +27,7 @@ Install the MCP extra: pip install redisvl[mcp] ``` -The extra installs FastMCP 2.12.1 or later, below 4. If you pin FastMCP yourself, stay within that range: older releases are missing APIs the server depends on. +The extra installs FastMCP 2.12.1 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: diff --git a/tests/integration/test_mcp/test_profiles.py b/tests/integration/test_mcp/test_profiles.py index bf01b4cf..434a6919 100644 --- a/tests/integration/test_mcp/test_profiles.py +++ b/tests/integration/test_mcp/test_profiles.py @@ -258,8 +258,8 @@ 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.12.1,<4`, so a minor bump must not break this on presentation alone. assert set(schema["required"]) == {"query"} assert schema["additionalProperties"] is False From 9271c345164b7a9a8fb462a5d211a23b595ea5e8 Mon Sep 17 00:00:00 2001 From: Vishal Bala Date: Fri, 30 Oct 2026 17:03:53 +0100 Subject: [PATCH 3/3] fix(mcp): raise the fastmcp floor to 2.13.0 and gate the schema assertion on 3.x FastMCP 2.12.1 to 2.12.5 leave SSE clients hanging while they list tools (PrefectHQ/fastmcp#1903, fixed in 2.13.0), so 2.12.1 was not a working floor for the documented --transport sse server. FastMCP 2.x omits additionalProperties from tool schemas but still refuses extra arguments at call time, so assert the key only on 3.x. --- docs/user_guide/how_to_guides/mcp.md | 2 +- pyproject.toml | 5 +++-- tests/integration/test_mcp/test_profiles.py | 8 ++++++-- tests/unit/test_mcp/test_profiles_unit.py | 4 +++- uv.lock | 2 +- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/user_guide/how_to_guides/mcp.md b/docs/user_guide/how_to_guides/mcp.md index 03d51493..aeb86853 100644 --- a/docs/user_guide/how_to_guides/mcp.md +++ b/docs/user_guide/how_to_guides/mcp.md @@ -27,7 +27,7 @@ Install the MCP extra: pip install redisvl[mcp] ``` -The extra installs FastMCP 2.12.1 or later, below 4. If you pin FastMCP yourself, stay within that range, because the server does not work with older releases. +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: diff --git a/pyproject.toml b/pyproject.toml index e3a5e5a6..4e6a7ce5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,8 +45,9 @@ mcp = [ # Supporting that revision is its own piece of work. # 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, or reject the base_url the JWT verifier is built with. - "fastmcp>=2.12.1,<4", + # 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 434a6919..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 @@ -259,9 +260,12 @@ async def test_profile_advertises_only_its_exposed_arguments(started_server): 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 allowed anywhere in - # `>=2.12.1,<4`, so a minor bump must not break this on presentation alone. + # `>=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 90818cb1..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.12.1,<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" },