Skip to content

Commit 0f951d6

Browse files
committed
feat(deps): upgrade fastmcp from 2.x to 3.x
Upgrade fastmcp dependency from >=2.14.4,<3 to >=3.3.1,<4. Remove standalone mcp dependency (transitive via fastmcp 3.x). In FastMCP 3.x, @mcp.tool returns the original function instead of a FunctionTool wrapper. Update tests to call search_documents() directly instead of search_documents.fn(), and replace FunctionTool isinstance checks with plain callable/coroutine assertions. Signed-off-by: Major Hayden <major@redhat.com>
1 parent a82718b commit 0f951d6

5 files changed

Lines changed: 194 additions & 219 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2424

2525
### Changed
2626

27+
- Upgraded fastmcp from 2.x to 3.x (`>=3.3.1, <4`)
28+
- Removed standalone `mcp` dependency (pulled in transitively by fastmcp 3.x)
29+
- Updated smoke tests to match FastMCP 3.x decorator behavior (`@mcp.tool` now returns the original function)
2730
- CI workflow: extended ruff check and format to cover `src/`, `tests/`, and `demos/`
2831
- Makefile: `lint` target now runs `ruff check --fix` (was `ruff check`)
2932
- pytest: added `--cov=docs2db_mcp --cov-report=term-missing` to default options

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ classifiers = [
3131
]
3232

3333
dependencies = [
34-
"fastmcp >=2.14.4, <3",
35-
"mcp >=1.27.1",
34+
"fastmcp >=3.3.1, <4",
3635
"docs2db-api",
3736
"pydantic >=2.13.4",
3837
"pydantic-settings >=2.14.1",

tests/test_search_documents.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ async def test_valid_query_returns_chunks(self, mock_engine, sample_documents):
88
from docs2db_mcp.tools.search_documents import search_documents
99

1010
with patch("docs2db_mcp.tools.search_documents.get_engine", new=AsyncMock(return_value=mock_engine)):
11-
result = await search_documents.fn(query="RHEL 9 security features")
11+
result = await search_documents(query="RHEL 9 security features")
1212

1313
assert "chunks" in result
1414
assert "query_used" in result
@@ -21,7 +21,7 @@ async def test_chunk_structure(self, mock_engine):
2121
from docs2db_mcp.tools.search_documents import search_documents
2222

2323
with patch("docs2db_mcp.tools.search_documents.get_engine", new=AsyncMock(return_value=mock_engine)):
24-
result = await search_documents.fn(query="RHEL packages")
24+
result = await search_documents(query="RHEL packages")
2525

2626
assert result["num_results"] > 0
2727
chunk = result["chunks"][0]
@@ -35,7 +35,7 @@ async def test_chunk_data_maps_correctly(self, mock_engine, sample_documents):
3535
from docs2db_mcp.tools.search_documents import search_documents
3636

3737
with patch("docs2db_mcp.tools.search_documents.get_engine", new=AsyncMock(return_value=mock_engine)):
38-
result = await search_documents.fn(query="RHEL security")
38+
result = await search_documents(query="RHEL security")
3939

4040
first_chunk = result["chunks"][0]
4141
first_doc = sample_documents[0]
@@ -48,7 +48,7 @@ async def test_engine_called_with_correct_params(self, mock_engine):
4848

4949
mock_get_engine = AsyncMock(return_value=mock_engine)
5050
with patch("docs2db_mcp.tools.search_documents.get_engine", new=mock_get_engine):
51-
await search_documents.fn(
51+
await search_documents(
5252
query="test query",
5353
max_chunks=10,
5454
similarity_threshold=0.8,
@@ -67,7 +67,7 @@ async def test_default_params_forwarded(self, mock_engine):
6767

6868
mock_get_engine = AsyncMock(return_value=mock_engine)
6969
with patch("docs2db_mcp.tools.search_documents.get_engine", new=mock_get_engine):
70-
await search_documents.fn(query="test")
70+
await search_documents(query="test")
7171

7272
mock_engine.search_documents.assert_called_once_with(
7373
query="test",
@@ -81,15 +81,15 @@ async def test_query_used_is_original_query(self, mock_engine):
8181

8282
original_query = "How to configure firewalld in RHEL?"
8383
with patch("docs2db_mcp.tools.search_documents.get_engine", new=AsyncMock(return_value=mock_engine)):
84-
result = await search_documents.fn(query=original_query)
84+
result = await search_documents(query=original_query)
8585

8686
assert result["query_used"] == original_query
8787

8888
async def test_multiple_chunks_order_preserved(self, mock_engine, sample_documents):
8989
from docs2db_mcp.tools.search_documents import search_documents
9090

9191
with patch("docs2db_mcp.tools.search_documents.get_engine", new=AsyncMock(return_value=mock_engine)):
92-
result = await search_documents.fn(query="RHEL documentation")
92+
result = await search_documents(query="RHEL documentation")
9393

9494
assert result["num_results"] == len(sample_documents)
9595
for chunk, doc in zip(result["chunks"], sample_documents, strict=True):
@@ -106,7 +106,7 @@ async def test_empty_results_returns_zero_chunks(self):
106106
mock_engine.search_documents = AsyncMock(return_value=empty_result)
107107

108108
with patch("docs2db_mcp.tools.search_documents.get_engine", new=AsyncMock(return_value=mock_engine)):
109-
result = await search_documents.fn(query="nonexistent topic")
109+
result = await search_documents(query="nonexistent topic")
110110

111111
assert result["chunks"] == []
112112
assert result["num_results"] == 0
@@ -122,7 +122,7 @@ async def test_empty_results_preserves_query(self):
122122

123123
query = "obscure topic that returns nothing"
124124
with patch("docs2db_mcp.tools.search_documents.get_engine", new=AsyncMock(return_value=mock_engine)):
125-
result = await search_documents.fn(query=query)
125+
result = await search_documents(query=query)
126126

127127
assert result["query_used"] == query
128128

@@ -133,7 +133,7 @@ async def test_connection_error_returns_error_dict(self):
133133

134134
mock_get_engine = AsyncMock(side_effect=ConnectionError("Database unreachable"))
135135
with patch("docs2db_mcp.tools.search_documents.get_engine", new=mock_get_engine):
136-
result = await search_documents.fn(query="test query")
136+
result = await search_documents(query="test query")
137137

138138
assert "error" in result
139139
assert result["chunks"] == []
@@ -145,7 +145,7 @@ async def test_search_error_returns_error_dict(self, mock_engine):
145145

146146
mock_engine.search_documents = AsyncMock(side_effect=RuntimeError("Search failed"))
147147
with patch("docs2db_mcp.tools.search_documents.get_engine", new=AsyncMock(return_value=mock_engine)):
148-
result = await search_documents.fn(query="test query")
148+
result = await search_documents(query="test query")
149149

150150
assert "error" in result
151151
assert result["chunks"] == []
@@ -158,7 +158,7 @@ async def test_error_preserves_original_query(self):
158158
mock_get_engine = AsyncMock(side_effect=Exception("Unexpected error"))
159159
original_query = "RHEL 10 features"
160160
with patch("docs2db_mcp.tools.search_documents.get_engine", new=mock_get_engine):
161-
result = await search_documents.fn(query=original_query)
161+
result = await search_documents(query=original_query)
162162

163163
assert result["query_used"] == original_query
164164

@@ -167,7 +167,7 @@ async def test_timeout_error_handled_gracefully(self):
167167

168168
mock_get_engine = AsyncMock(side_effect=TimeoutError())
169169
with patch("docs2db_mcp.tools.search_documents.get_engine", new=mock_get_engine):
170-
result = await search_documents.fn(query="slow query")
170+
result = await search_documents(query="slow query")
171171

172172
assert "error" in result
173173
assert result["chunks"] == []
@@ -178,7 +178,7 @@ async def test_generic_exception_handled_gracefully(self):
178178

179179
mock_get_engine = AsyncMock(side_effect=Exception("Something went wrong"))
180180
with patch("docs2db_mcp.tools.search_documents.get_engine", new=mock_get_engine):
181-
result = await search_documents.fn(query="test")
181+
result = await search_documents(query="test")
182182

183183
assert "error" in result
184184
assert "Something went wrong" in result["error"]
@@ -199,7 +199,7 @@ async def test_optional_fields_default_to_none(self):
199199
mock_engine.search_documents = AsyncMock(return_value=mock_result)
200200

201201
with patch("docs2db_mcp.tools.search_documents.get_engine", new=AsyncMock(return_value=mock_engine)):
202-
result = await search_documents.fn(query="minimal test")
202+
result = await search_documents(query="minimal test")
203203

204204
chunk = result["chunks"][0]
205205
assert chunk["text"] == "Minimal document chunk"
@@ -212,7 +212,7 @@ async def test_similarity_is_float(self, mock_engine):
212212
from docs2db_mcp.tools.search_documents import search_documents
213213

214214
with patch("docs2db_mcp.tools.search_documents.get_engine", new=AsyncMock(return_value=mock_engine)):
215-
result = await search_documents.fn(query="test")
215+
result = await search_documents(query="test")
216216

217217
for chunk in result["chunks"]:
218218
assert isinstance(chunk["similarity"], float)
@@ -221,14 +221,14 @@ async def test_rerank_score_preserved(self, mock_engine, sample_documents):
221221
from docs2db_mcp.tools.search_documents import search_documents
222222

223223
with patch("docs2db_mcp.tools.search_documents.get_engine", new=AsyncMock(return_value=mock_engine)):
224-
result = await search_documents.fn(query="test")
224+
result = await search_documents(query="test")
225225

226226
assert result["chunks"][0]["rerank_score"] == sample_documents[0]["rerank_score"]
227227

228228
async def test_vector_similarity_preserved(self, mock_engine, sample_documents):
229229
from docs2db_mcp.tools.search_documents import search_documents
230230

231231
with patch("docs2db_mcp.tools.search_documents.get_engine", new=AsyncMock(return_value=mock_engine)):
232-
result = await search_documents.fn(query="test")
232+
result = await search_documents(query="test")
233233

234234
assert result["chunks"][0]["vector_similarity"] == sample_documents[0]["vector_similarity"]

tests/test_smoke.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,15 @@ def test_shutdown_engine_is_async(self):
4545
assert asyncio.iscoroutinefunction(shutdown_engine)
4646

4747
def test_search_documents_tool_exists(self):
48-
from fastmcp.tools.tool import FunctionTool
49-
5048
from docs2db_mcp.tools import search_documents
5149

52-
assert isinstance(search_documents, FunctionTool)
53-
assert search_documents.name == "search_documents"
50+
assert callable(search_documents)
51+
assert search_documents.__name__ == "search_documents"
5452

5553
def test_search_documents_fn_is_async(self):
5654
from docs2db_mcp.tools import search_documents
5755

58-
assert asyncio.iscoroutinefunction(search_documents.fn)
56+
assert asyncio.iscoroutinefunction(search_documents)
5957

6058

6159
class TestConfigDefaults:
@@ -143,10 +141,8 @@ def test_mcp_server_is_configured(self):
143141
assert mcp is not None
144142

145143
def test_search_documents_registered(self):
146-
from fastmcp.tools.tool import FunctionTool
147-
148144
from docs2db_mcp.tools import search_documents
149145

150-
assert isinstance(search_documents, FunctionTool)
151-
assert search_documents.name == "search_documents"
152-
assert search_documents.enabled
146+
assert callable(search_documents)
147+
assert search_documents.__name__ == "search_documents"
148+
assert asyncio.iscoroutinefunction(search_documents)

0 commit comments

Comments
 (0)