From 494294046f82901c05d0173300786514759f62bd Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Fri, 4 Sep 2026 13:11:06 +0300 Subject: [PATCH] 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. --- CHANGELOG.md | 2 ++ haiku_rag_slim/haiku/rag/mcp.py | 4 +-- haiku_rag_slim/pyproject.toml | 2 +- tests/test_mcp.py | 28 ++++++++------- uv.lock | 62 +++++++++++++++++++-------------- 5 files changed, 55 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41411c13..848bf844 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/haiku_rag_slim/haiku/rag/mcp.py b/haiku_rag_slim/haiku/rag/mcp.py index 5da35d47..b3e8d680 100644 --- a/haiku_rag_slim/haiku/rag/mcp.py +++ b/haiku_rag_slim/haiku/rag/mcp.py @@ -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( diff --git a/haiku_rag_slim/pyproject.toml b/haiku_rag_slim/pyproject.toml index e438dbfd..b3e5dbc6 100644 --- a/haiku_rag_slim/pyproject.toml +++ b/haiku_rag_slim/pyproject.toml @@ -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", diff --git a/tests/test_mcp.py b/tests/test_mcp.py index 4f1160b8..002723cc 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -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 diff --git a/uv.lock b/uv.lock index 2754fbb2..4d9636e2 100644 --- a/uv.lock +++ b/uv.lock @@ -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]]