Move to fastmcp 4 and MCP SDK 2

fastmcp>=4.0.2,<5.0.0. Protocol types are snake_case (read_only_hint,
mime_type, input_schema, server_info); the camelCase names warn and go
in fastmcp 5, so the next major is an explicit upgrade. The client
defaults to the sessionless protocol, where initialize_result is None,
so the tests read client.instructions and client.server_info, which both
protocol modes populate. The server answers either mode.
This commit is contained in:
Yiorgis Gozadinos 2026-09-04 13:11:06 +03:00
parent eb9995e9b7
commit 494294046f
No known key found for this signature in database
5 changed files with 55 additions and 43 deletions

View file

@ -18,6 +18,8 @@
### Changed
- `fastmcp>=4.0.2,<5.0.0`, on MCP Python SDK 2. The MCP server answers both the
session-based and the sessionless (2026-07-28) protocol.
- Default models are `ollama:qwen3.8`: `ModelConfig`, `qa.model`,
`processing.title_model` (was `ollama:gpt-oss`) and
`processing.conversion_options.picture_description.model` (was

View file

@ -50,7 +50,7 @@ Sources = Annotated[
def _read_only(title: str) -> ToolAnnotations:
return ToolAnnotations(title=title, readOnlyHint=True, openWorldHint=False)
return ToolAnnotations(title=title, read_only_hint=True, open_world_hint=False)
def _decode_image(image_base64: str) -> bytes:
@ -150,7 +150,7 @@ def _search_result(results: list[SearchResult], covers_multiple: bool) -> ToolRe
ImageContent(
type="image",
data=base64.b64encode(picture.data).decode("ascii"),
mimeType="image/png",
mime_type="image/png",
)
)
return ToolResult(

View file

@ -40,7 +40,7 @@ dependencies = [
"docling-core>=2.82.0,<3.0.0",
"httpx>=0.28.1",
"jinja2>=3.1.0",
"fastmcp>=3.3.0",
"fastmcp>=4.0.2,<5.0.0",
"lancedb==0.37.1",
"pathspec>=1.0.4",
"pydantic>=2.12.5",

View file

@ -506,7 +506,7 @@ class TestMCPSearchResultShape:
images = [block for block in rest if isinstance(block, ImageContent)]
labels = [block.text for block in rest if isinstance(block, TextContent)]
assert len(images) == 2
assert all(image.mimeType == "image/png" for image in images)
assert all(image.mime_type == "image/png" for image in images)
assert [
label for label in labels if "[c1]" in label and "#/pictures/0" in label
]
@ -610,10 +610,12 @@ class TestMCPDescribesItself:
from fastmcp import Client
async with Client(create_mcp_server(mcp_db)) as client:
init = client.initialize_result
instructions = client.instructions
server_info = client.server_info
assert init.instructions
assert init.serverInfo.version == metadata.version("haiku.rag-slim")
assert instructions
assert server_info is not None
assert server_info.version == metadata.version("haiku.rag-slim")
@pytest.mark.asyncio
async def test_instructions_name_the_collections_when_covering_several(
@ -624,10 +626,10 @@ class TestMCPDescribesItself:
from haiku.rag.client.scope import DatabaseScope
async with Client(_covering_all(two_dbs)) as client:
covering_both = client.initialize_result.instructions
covering_both = client.instructions
one = DatabaseScope.resolve(two_dbs, database_name="alpha")
async with Client(_mcp_covering(one, two_dbs)) as client:
covering_one = client.initialize_result.instructions
covering_one = client.instructions
assert "alpha" in covering_both
assert "beta" in covering_both
@ -638,9 +640,9 @@ class TestMCPDescribesItself:
from fastmcp import Client
async with Client(create_mcp_server(mcp_db)) as client:
full = client.initialize_result.instructions.splitlines()
full = client.instructions.splitlines()
async with Client(create_mcp_server(mcp_db, agents=False)) as client:
without = client.initialize_result.instructions.splitlines()
without = client.instructions.splitlines()
assert set(without) < set(full)
assert len(without) == len(full) - 1
@ -655,9 +657,9 @@ class TestMCPDescribesItself:
config.prompts.domain_preamble = "Everything here is about zebras."
async with Client(create_mcp_server(mcp_db, config=config)) as client:
with_preamble = client.initialize_result.instructions
with_preamble = client.instructions
async with Client(create_mcp_server(mcp_db)) as client:
without = client.initialize_result.instructions
without = client.instructions
assert "Everything here is about zebras." in with_preamble
assert "zebras" not in without
@ -672,8 +674,8 @@ class TestMCPDescribesItself:
assert len(tools) == 8
for tool in tools:
assert tool.annotations is not None, tool.name
assert tool.annotations.readOnlyHint is True, tool.name
assert tool.annotations.openWorldHint is False, tool.name
assert tool.annotations.read_only_hint is True, tool.name
assert tool.annotations.open_world_hint is False, tool.name
assert tool.annotations.title, tool.name
@pytest.mark.asyncio
@ -686,7 +688,7 @@ class TestMCPDescribesItself:
undescribed = [
f"{tool.name}.{name}"
for tool in tools
for name, schema in tool.inputSchema.get("properties", {}).items()
for name, schema in tool.input_schema.get("properties", {}).items()
if not schema.get("description")
]
assert len(tools) == 8

62
uv.lock
View file

@ -1209,21 +1209,22 @@ wheels = [
[[package]]
name = "fastmcp"
version = "3.3.1"
version = "4.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "fastmcp-slim", extra = ["client", "server"] },
]
sdist = { url = "https://files.pythonhosted.org/packages/3b/a9/5c5a01b6abd5346bf60b97cfd29e4a86661940c27dd562bfcda07fd03519/fastmcp-3.3.1.tar.gz", hash = "sha256:979362ea557de42a5f40342563c7e4b236bcc8e7cd192715f50030695d1a71cd", size = 28681699, upload-time = "2026-05-15T15:50:39.673Z" }
sdist = { url = "https://files.pythonhosted.org/packages/37/1c/981a1854f91a08872f4b8b9a627d5d751cafc1340d29b21b747f0b520b0a/fastmcp-4.0.2.tar.gz", hash = "sha256:60d5c5ead3b6a117bfada5c0f95fe5c1aba53d1577079ecbdf42eeff0cd9b931", size = 42306015, upload-time = "2026-09-02T23:28:08.386Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9f/11/6b1bdada6ccfe647d615ae63f9106f8136aec17971e9361546af01c7d38e/fastmcp-3.3.1-py3-none-any.whl", hash = "sha256:862440c5c4d281363a5995eee59d77f0f7cac1f18869038729cecf03b02fc522", size = 7903, upload-time = "2026-05-15T15:50:36.424Z" },
{ url = "https://files.pythonhosted.org/packages/58/3f/b97cfb92e0d6db8232c67c258117cd0dd9def86c8b472270bd7196d5cd9d/fastmcp-4.0.2-py3-none-any.whl", hash = "sha256:9075e64a94634ad660971ed14374c87be06f2a16a921028ca87987e6aa2f3bfa", size = 8078, upload-time = "2026-09-02T23:28:03.777Z" },
]
[[package]]
name = "fastmcp-slim"
version = "3.3.1"
version = "4.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mcp-types" },
{ name = "platformdirs" },
{ name = "pydantic", extra = ["email"] },
{ name = "pydantic-settings" },
@ -1231,26 +1232,28 @@ dependencies = [
{ name = "rich" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d1/a0/627103e517e1d0d6f1eec633d5662d13e776f01b45ad188e4f5f7478b438/fastmcp_slim-3.3.1.tar.gz", hash = "sha256:0957835fc59452e143ab2f4b7836d2d2df9b2d9958408edc79ba8b56232b2a88", size = 567007, upload-time = "2026-05-15T15:50:10.426Z" }
sdist = { url = "https://files.pythonhosted.org/packages/9c/7d/c2597734e3a0859d62c9d8f6f35067d1e296537512e280db3af19204be64/fastmcp_slim-4.0.2.tar.gz", hash = "sha256:86b99bdcb872b52d964c79bc6d43ce79f40ed5538b589d102792b4a7cf3947f4", size = 684052, upload-time = "2026-09-02T23:27:39.868Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7a/ee/97047f4cc2d7b1d46670d08d8ad01a96e7a748cc01c0b4b351ad8eddbc7a/fastmcp_slim-3.3.1-py3-none-any.whl", hash = "sha256:6cf1c2d77e3adb0d409d6825ed6b0b2a999062973e00b8eea03bd48bf9b4c043", size = 738644, upload-time = "2026-05-15T15:50:08.336Z" },
{ url = "https://files.pythonhosted.org/packages/fa/c0/c022eba3a25ebb56111de1b5f76fbfca81925def58880464263582acfcd8/fastmcp_slim-4.0.2-py3-none-any.whl", hash = "sha256:6bd5b5885628f73263fa2247ea1d26e4a514499a6e079ee3e340cd03a7fe5ed8", size = 858100, upload-time = "2026-09-02T23:27:38.459Z" },
]
[package.optional-dependencies]
client = [
{ name = "authlib" },
{ name = "exceptiongroup" },
{ name = "httpx" },
{ name = "httpx2" },
{ name = "mcp" },
{ name = "opentelemetry-api" },
{ name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] },
{ name = "starlette" },
]
server = [
{ name = "authlib" },
{ name = "cyclopts" },
{ name = "exceptiongroup" },
{ name = "griffelib" },
{ name = "httpx" },
{ name = "httpx2" },
{ name = "joserfc" },
{ name = "jsonref" },
{ name = "jsonschema-path" },
{ name = "mcp" },
@ -1261,6 +1264,7 @@ server = [
{ name = "pyperclip" },
{ name = "python-multipart" },
{ name = "pyyaml" },
{ name = "starlette" },
{ name = "uncalled-for" },
{ name = "uvicorn" },
{ name = "watchfiles" },
@ -1755,7 +1759,7 @@ requires-dist = [
{ name = "docling", marker = "extra == 'docling'", specifier = ">=2.102.2,<3.0.0" },
{ name = "docling-core", specifier = ">=2.82.0,<3.0.0" },
{ name = "fastapi", marker = "extra == 'ingester'", specifier = ">=0.125" },
{ name = "fastmcp", specifier = ">=3.3.0" },
{ name = "fastmcp", specifier = ">=4.0.2,<5.0.0" },
{ name = "haiku-rag-slim", extras = ["s3"], marker = "extra == 'ingester'", editable = "haiku_rag_slim" },
{ name = "httpx", specifier = ">=0.28.1" },
{ name = "jinja2", specifier = ">=3.1.0" },
@ -1887,15 +1891,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[[package]]
name = "httpx-sse"
version = "0.4.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" },
]
[[package]]
name = "httpx2"
version = "2.8.0"
@ -2590,15 +2585,15 @@ wheels = [
[[package]]
name = "mcp"
version = "1.28.1"
version = "2.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "httpx" },
{ name = "httpx-sse" },
{ name = "httpx2" },
{ name = "jsonschema" },
{ name = "mcp-types" },
{ name = "opentelemetry-api" },
{ name = "pydantic" },
{ name = "pydantic-settings" },
{ name = "pyjwt", extra = ["crypto"] },
{ name = "python-multipart" },
{ name = "pywin32", marker = "sys_platform == 'win32'" },
@ -2608,9 +2603,22 @@ dependencies = [
{ name = "typing-inspection" },
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d4/6e/21fb8e5d579dbe21d96ea4d5034200d46d8bdf2261053b5bd041f3c2f612/mcp-2.1.1.tar.gz", hash = "sha256:50b7ba1ebbe117008ea7bdd288234043e69c20b403d6851d19661e6d431a75ef", size = 3984589, upload-time = "2026-08-25T16:14:02.376Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" },
{ url = "https://files.pythonhosted.org/packages/50/af/8644cc5fa26a59afd2df2e98eeb19e72926887fa4b7441aba4ff661140db/mcp-2.1.1-py3-none-any.whl", hash = "sha256:1c6c31c5d6471c58db76af3af8af67f46d11d01f0a59077d0a308cbdb3d3e915", size = 357912, upload-time = "2026-08-25T16:13:59.024Z" },
]
[[package]]
name = "mcp-types"
version = "2.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6a/dd/1c4417dc0b722c23a1669032d5f044e41170fe5d4773b488a50fcce98c32/mcp_types-2.1.1.tar.gz", hash = "sha256:77dcbe48fba73cca71a673f2646a5f037a017b7a0a07ac89cec1113028890eda", size = 66674, upload-time = "2026-08-25T16:14:03.861Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/71/d0/242e63c510f4a17381f55b1549a3f94f5687a0595984febd2b6f87a687a0/mcp_types-2.1.1-py3-none-any.whl", hash = "sha256:26f9f7f03f2a5730717a5b98e2ab7eb640ac352d05a00cdc725c311864778295", size = 69656, upload-time = "2026-08-25T16:14:00.667Z" },
]
[[package]]
@ -5543,11 +5551,11 @@ wheels = [
[[package]]
name = "uncalled-for"
version = "0.2.0"
version = "0.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/02/7c/b5b7d8136f872e3f13b0584e576886de0489d7213a12de6bebf29ff6ebfc/uncalled_for-0.2.0.tar.gz", hash = "sha256:b4f8fdbcec328c5a113807d653e041c5094473dd4afa7c34599ace69ccb7e69f", size = 49488, upload-time = "2026-02-27T17:40:58.137Z" }
sdist = { url = "https://files.pythonhosted.org/packages/6b/5a/92ce0b3ea5481915f55da994c2c2c5f7a3c09949afde196ee89f8ab961aa/uncalled_for-0.4.0.tar.gz", hash = "sha256:335b95bd2422332ec210d518f314a16e4c640921c39fc8bf2ad095bd3538f4af", size = 56979, upload-time = "2026-08-10T14:51:46.247Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ff/7f/4320d9ce3be404e6310b915c3629fe27bf1e2f438a1a7a3cb0396e32e9a9/uncalled_for-0.2.0-py3-none-any.whl", hash = "sha256:2c0bd338faff5f930918f79e7eb9ff48290df2cb05fcc0b40a7f334e55d4d85f", size = 11351, upload-time = "2026-02-27T17:40:56.804Z" },
{ url = "https://files.pythonhosted.org/packages/a2/40/97cec87c077eb3291fc7905e6633e08b7ca593c57d30238444bcb6bb3d53/uncalled_for-0.4.0-py3-none-any.whl", hash = "sha256:16c4bb3337532e4bd5569adc192285976f3ad5305402256d34c67a12b5c968bd", size = 15502, upload-time = "2026-08-10T14:51:45.068Z" },
]
[[package]]