@@ -1221,3 +1221,143 @@ async def route2(request):
12211221 route_paths = [route .path for route in routes ] # type: ignore[attr-defined]
12221222 assert "/route1" in route_paths
12231223 assert "/route2" in route_paths
1224+
1225+
1226+ class TestDeeplyNestedMount :
1227+ """Test deeply nested mount scenarios (3+ levels deep).
1228+
1229+ This tests the fix for https://github.com/jlowin/fastmcp/issues/2583
1230+ where tools/resources/prompts mounted more than 2 levels deep would fail
1231+ to invoke even though they were correctly listed.
1232+ """
1233+
1234+ async def test_three_level_nested_tool_invocation (self ):
1235+ """Test invoking tools from servers mounted 3 levels deep."""
1236+ root = FastMCP ("root" )
1237+ middle = FastMCP ("middle" )
1238+ leaf = FastMCP ("leaf" )
1239+
1240+ @leaf .tool
1241+ def add (a : int , b : int ) -> int :
1242+ return a + b
1243+
1244+ @middle .tool
1245+ def multiply (a : int , b : int ) -> int :
1246+ return a * b
1247+
1248+ middle .mount (leaf , prefix = "leaf" )
1249+ root .mount (middle , prefix = "middle" )
1250+
1251+ async with Client (root ) as client :
1252+ # Tool at level 2 should work
1253+ result = await client .call_tool ("middle_multiply" , {"a" : 3 , "b" : 4 })
1254+ assert result .data == 12
1255+
1256+ # Tool at level 3 should also work (this was the bug)
1257+ result = await client .call_tool ("middle_leaf_add" , {"a" : 5 , "b" : 7 })
1258+ assert result .data == 12
1259+
1260+ async def test_three_level_nested_resource_invocation (self ):
1261+ """Test reading resources from servers mounted 3 levels deep."""
1262+ root = FastMCP ("root" )
1263+ middle = FastMCP ("middle" )
1264+ leaf = FastMCP ("leaf" )
1265+
1266+ @leaf .resource ("leaf://data" )
1267+ def leaf_data () -> str :
1268+ return "leaf data"
1269+
1270+ @middle .resource ("middle://data" )
1271+ def middle_data () -> str :
1272+ return "middle data"
1273+
1274+ middle .mount (leaf , prefix = "leaf" )
1275+ root .mount (middle , prefix = "middle" )
1276+
1277+ async with Client (root ) as client :
1278+ # Resource at level 2 should work
1279+ result = await client .read_resource ("middle://middle/data" )
1280+ assert result [0 ].text == "middle data"
1281+
1282+ # Resource at level 3 should also work
1283+ result = await client .read_resource ("leaf://middle/leaf/data" )
1284+ assert result [0 ].text == "leaf data"
1285+
1286+ async def test_three_level_nested_resource_template_invocation (self ):
1287+ """Test reading resource templates from servers mounted 3 levels deep."""
1288+ root = FastMCP ("root" )
1289+ middle = FastMCP ("middle" )
1290+ leaf = FastMCP ("leaf" )
1291+
1292+ @leaf .resource ("leaf://item/{id}" )
1293+ def leaf_item (id : str ) -> str :
1294+ return f"leaf item { id } "
1295+
1296+ @middle .resource ("middle://item/{id}" )
1297+ def middle_item (id : str ) -> str :
1298+ return f"middle item { id } "
1299+
1300+ middle .mount (leaf , prefix = "leaf" )
1301+ root .mount (middle , prefix = "middle" )
1302+
1303+ async with Client (root ) as client :
1304+ # Resource template at level 2 should work
1305+ result = await client .read_resource ("middle://middle/item/42" )
1306+ assert result [0 ].text == "middle item 42"
1307+
1308+ # Resource template at level 3 should also work
1309+ result = await client .read_resource ("leaf://middle/leaf/item/99" )
1310+ assert result [0 ].text == "leaf item 99"
1311+
1312+ async def test_three_level_nested_prompt_invocation (self ):
1313+ """Test getting prompts from servers mounted 3 levels deep."""
1314+ root = FastMCP ("root" )
1315+ middle = FastMCP ("middle" )
1316+ leaf = FastMCP ("leaf" )
1317+
1318+ @leaf .prompt
1319+ def leaf_prompt (name : str ) -> str :
1320+ return f"Hello from leaf: { name } "
1321+
1322+ @middle .prompt
1323+ def middle_prompt (name : str ) -> str :
1324+ return f"Hello from middle: { name } "
1325+
1326+ middle .mount (leaf , prefix = "leaf" )
1327+ root .mount (middle , prefix = "middle" )
1328+
1329+ async with Client (root ) as client :
1330+ # Prompt at level 2 should work
1331+ result = await client .get_prompt ("middle_middle_prompt" , {"name" : "World" })
1332+ assert "Hello from middle: World" in result .messages [0 ].content .text # type: ignore[union-attr]
1333+
1334+ # Prompt at level 3 should also work
1335+ result = await client .get_prompt (
1336+ "middle_leaf_leaf_prompt" , {"name" : "Test" }
1337+ )
1338+ assert "Hello from leaf: Test" in result .messages [0 ].content .text # type: ignore[union-attr]
1339+
1340+ async def test_four_level_nested_tool_invocation (self ):
1341+ """Test invoking tools from servers mounted 4 levels deep."""
1342+ root = FastMCP ("root" )
1343+ level1 = FastMCP ("level1" )
1344+ level2 = FastMCP ("level2" )
1345+ level3 = FastMCP ("level3" )
1346+
1347+ @level3 .tool
1348+ def deep_tool () -> str :
1349+ return "very deep"
1350+
1351+ level2 .mount (level3 , prefix = "l3" )
1352+ level1 .mount (level2 , prefix = "l2" )
1353+ root .mount (level1 , prefix = "l1" )
1354+
1355+ async with Client (root ) as client :
1356+ # Verify tool is listed
1357+ tools = await client .list_tools ()
1358+ tool_names = [t .name for t in tools ]
1359+ assert "l1_l2_l3_deep_tool" in tool_names
1360+
1361+ # Tool at level 4 should work
1362+ result = await client .call_tool ("l1_l2_l3_deep_tool" , {})
1363+ assert result .data == "very deep"
0 commit comments