Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
54bdf2c
1
Parent(s): e97c2b8
Updated list_tools
Browse files
src/fastmcp/server/middleware.py
CHANGED
|
@@ -144,6 +144,14 @@ class MCPMiddleware:
|
|
| 144 |
handler = partial(self.on_read_resource, call_next=handler)
|
| 145 |
case "prompts/get":
|
| 146 |
handler = partial(self.on_get_prompt, call_next=handler)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
match context.type:
|
| 149 |
case "request":
|
|
@@ -203,3 +211,26 @@ class MCPMiddleware:
|
|
| 203 |
call_next: CallNext[mt.ListToolsRequest, ListToolsResult],
|
| 204 |
) -> ListToolsResult:
|
| 205 |
return await call_next(context)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
handler = partial(self.on_read_resource, call_next=handler)
|
| 145 |
case "prompts/get":
|
| 146 |
handler = partial(self.on_get_prompt, call_next=handler)
|
| 147 |
+
case "tools/list":
|
| 148 |
+
handler = partial(self.on_list_tools, call_next=handler)
|
| 149 |
+
case "resources/list":
|
| 150 |
+
handler = partial(self.on_list_resources, call_next=handler)
|
| 151 |
+
case "resource-templates/list":
|
| 152 |
+
handler = partial(self.on_list_resource_templates, call_next=handler)
|
| 153 |
+
case "prompts/list":
|
| 154 |
+
handler = partial(self.on_list_prompts, call_next=handler)
|
| 155 |
|
| 156 |
match context.type:
|
| 157 |
case "request":
|
|
|
|
| 211 |
call_next: CallNext[mt.ListToolsRequest, ListToolsResult],
|
| 212 |
) -> ListToolsResult:
|
| 213 |
return await call_next(context)
|
| 214 |
+
|
| 215 |
+
async def on_list_resources(
|
| 216 |
+
self,
|
| 217 |
+
context: MiddlewareContext[mt.ListResourcesRequest],
|
| 218 |
+
call_next: CallNext[mt.ListResourcesRequest, ListResourcesResult],
|
| 219 |
+
) -> ListResourcesResult:
|
| 220 |
+
return await call_next(context)
|
| 221 |
+
|
| 222 |
+
async def on_list_resource_templates(
|
| 223 |
+
self,
|
| 224 |
+
context: MiddlewareContext[mt.ListResourceTemplatesRequest],
|
| 225 |
+
call_next: CallNext[
|
| 226 |
+
mt.ListResourceTemplatesRequest, ListResourceTemplatesResult
|
| 227 |
+
],
|
| 228 |
+
) -> ListResourceTemplatesResult:
|
| 229 |
+
return await call_next(context)
|
| 230 |
+
|
| 231 |
+
async def on_list_prompts(
|
| 232 |
+
self,
|
| 233 |
+
context: MiddlewareContext[mt.ListPromptsRequest],
|
| 234 |
+
call_next: CallNext[mt.ListPromptsRequest, ListPromptsResult],
|
| 235 |
+
) -> ListPromptsResult:
|
| 236 |
+
return await call_next(context)
|
src/fastmcp/server/server.py
CHANGED
|
@@ -341,7 +341,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 341 |
|
| 342 |
async def get_tools(self) -> dict[str, Tool]:
|
| 343 |
"""Get all registered tools, indexed by registered key."""
|
| 344 |
-
|
|
|
|
| 345 |
|
| 346 |
async def get_tool(self, key: str) -> Tool:
|
| 347 |
tools = await self.get_tools()
|
|
@@ -515,7 +516,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 515 |
tools = await self._middleware_list_tools()
|
| 516 |
return [tool.to_mcp_tool(name=tool.name) for tool in tools]
|
| 517 |
|
| 518 |
-
async def _middleware_list_tools(self) ->
|
| 519 |
"""
|
| 520 |
List all available tools, in the format expected by the low-level MCP
|
| 521 |
server.
|
|
@@ -524,13 +525,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 524 |
|
| 525 |
async def _handler(
|
| 526 |
context: MiddlewareContext[mcp.types.ListToolsRequest],
|
| 527 |
-
) -> list[
|
| 528 |
tools = await self._list_tools()
|
| 529 |
|
| 530 |
-
mcp_tools: list[
|
| 531 |
-
for
|
| 532 |
if self._should_enable_component(tool):
|
| 533 |
-
mcp_tools.append(tool
|
| 534 |
|
| 535 |
return mcp_tools
|
| 536 |
|
|
@@ -547,13 +548,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 547 |
# Apply the middleware chain.
|
| 548 |
return await self._apply_middleware(mw_context, _handler)
|
| 549 |
|
| 550 |
-
async def _list_tools(self, apply_middleware: bool = True) ->
|
| 551 |
"""
|
| 552 |
List all available tools.
|
| 553 |
"""
|
| 554 |
|
| 555 |
if (tools := self._cache.get("tools")) is self._cache.NOT_FOUND:
|
| 556 |
-
tools:
|
| 557 |
|
| 558 |
# iterate such that new mounts overwrite older ones
|
| 559 |
for mounted_server in self._mounted_servers:
|
|
@@ -566,18 +567,17 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 566 |
server_tools = await mounted_server.server._list_tools()
|
| 567 |
# Apply prefix to each tool key if prefix exists and is not empty
|
| 568 |
if mounted_server.prefix:
|
| 569 |
-
for tool in server_tools
|
| 570 |
tool = tool.with_key(f"{mounted_server.prefix}_{tool.key}")
|
| 571 |
-
tools
|
| 572 |
else:
|
| 573 |
-
tools.
|
| 574 |
-
tools.update(server_tools)
|
| 575 |
except Exception as e:
|
| 576 |
logger.warning(
|
| 577 |
f"Failed to get tools from mounted server '{mounted_server.prefix}': {e}"
|
| 578 |
)
|
| 579 |
continue
|
| 580 |
-
tools.
|
| 581 |
self._cache.set("tools", tools)
|
| 582 |
return tools
|
| 583 |
|
|
|
|
| 341 |
|
| 342 |
async def get_tools(self) -> dict[str, Tool]:
|
| 343 |
"""Get all registered tools, indexed by registered key."""
|
| 344 |
+
tools = await self._list_tools(apply_middleware=False)
|
| 345 |
+
return {tool.key: tool for tool in tools}
|
| 346 |
|
| 347 |
async def get_tool(self, key: str) -> Tool:
|
| 348 |
tools = await self.get_tools()
|
|
|
|
| 516 |
tools = await self._middleware_list_tools()
|
| 517 |
return [tool.to_mcp_tool(name=tool.name) for tool in tools]
|
| 518 |
|
| 519 |
+
async def _middleware_list_tools(self) -> list[Tool]:
|
| 520 |
"""
|
| 521 |
List all available tools, in the format expected by the low-level MCP
|
| 522 |
server.
|
|
|
|
| 525 |
|
| 526 |
async def _handler(
|
| 527 |
context: MiddlewareContext[mcp.types.ListToolsRequest],
|
| 528 |
+
) -> list[Tool]:
|
| 529 |
tools = await self._list_tools()
|
| 530 |
|
| 531 |
+
mcp_tools: list[Tool] = []
|
| 532 |
+
for tool in tools:
|
| 533 |
if self._should_enable_component(tool):
|
| 534 |
+
mcp_tools.append(tool)
|
| 535 |
|
| 536 |
return mcp_tools
|
| 537 |
|
|
|
|
| 548 |
# Apply the middleware chain.
|
| 549 |
return await self._apply_middleware(mw_context, _handler)
|
| 550 |
|
| 551 |
+
async def _list_tools(self, apply_middleware: bool = True) -> list[Tool]:
|
| 552 |
"""
|
| 553 |
List all available tools.
|
| 554 |
"""
|
| 555 |
|
| 556 |
if (tools := self._cache.get("tools")) is self._cache.NOT_FOUND:
|
| 557 |
+
tools: list[Tool] = []
|
| 558 |
|
| 559 |
# iterate such that new mounts overwrite older ones
|
| 560 |
for mounted_server in self._mounted_servers:
|
|
|
|
| 567 |
server_tools = await mounted_server.server._list_tools()
|
| 568 |
# Apply prefix to each tool key if prefix exists and is not empty
|
| 569 |
if mounted_server.prefix:
|
| 570 |
+
for tool in server_tools:
|
| 571 |
tool = tool.with_key(f"{mounted_server.prefix}_{tool.key}")
|
| 572 |
+
tools.append(tool)
|
| 573 |
else:
|
| 574 |
+
tools.extend(server_tools)
|
|
|
|
| 575 |
except Exception as e:
|
| 576 |
logger.warning(
|
| 577 |
f"Failed to get tools from mounted server '{mounted_server.prefix}': {e}"
|
| 578 |
)
|
| 579 |
continue
|
| 580 |
+
tools.extend(self._tool_manager.get_tools().values())
|
| 581 |
self._cache.set("tools", tools)
|
| 582 |
return tools
|
| 583 |
|
tests/server/middleware/test_middleware.py
CHANGED
|
@@ -7,7 +7,7 @@ import pytest
|
|
| 7 |
|
| 8 |
from fastmcp import Client, FastMCP
|
| 9 |
from fastmcp.server.context import Context
|
| 10 |
-
from fastmcp.server.middleware
|
| 11 |
|
| 12 |
|
| 13 |
@dataclass
|
|
@@ -194,3 +194,43 @@ class TestMiddlewareHooks:
|
|
| 194 |
assert recording_middleware.assert_called(hook="on_message", times=1)
|
| 195 |
assert recording_middleware.assert_called(hook="on_request", times=1)
|
| 196 |
assert recording_middleware.assert_called(hook="on_list_tools", times=1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
from fastmcp import Client, FastMCP
|
| 9 |
from fastmcp.server.context import Context
|
| 10 |
+
from fastmcp.server.middleware import MCPMiddleware, MiddlewareContext
|
| 11 |
|
| 12 |
|
| 13 |
@dataclass
|
|
|
|
| 194 |
assert recording_middleware.assert_called(hook="on_message", times=1)
|
| 195 |
assert recording_middleware.assert_called(hook="on_request", times=1)
|
| 196 |
assert recording_middleware.assert_called(hook="on_list_tools", times=1)
|
| 197 |
+
|
| 198 |
+
async def test_list_resources(
|
| 199 |
+
self, mcp_server: FastMCP, recording_middleware: RecordingMiddleware
|
| 200 |
+
):
|
| 201 |
+
async with Client(mcp_server) as client:
|
| 202 |
+
await client.list_resources()
|
| 203 |
+
|
| 204 |
+
assert recording_middleware.assert_called(times=3)
|
| 205 |
+
assert recording_middleware.assert_called(method="resources/list", times=3)
|
| 206 |
+
assert recording_middleware.assert_called(hook="on_message", times=1)
|
| 207 |
+
assert recording_middleware.assert_called(hook="on_request", times=1)
|
| 208 |
+
assert recording_middleware.assert_called(hook="on_list_resources", times=1)
|
| 209 |
+
|
| 210 |
+
async def test_list_resource_templates(
|
| 211 |
+
self, mcp_server: FastMCP, recording_middleware: RecordingMiddleware
|
| 212 |
+
):
|
| 213 |
+
async with Client(mcp_server) as client:
|
| 214 |
+
await client.list_resource_templates()
|
| 215 |
+
|
| 216 |
+
assert recording_middleware.assert_called(times=3)
|
| 217 |
+
assert recording_middleware.assert_called(
|
| 218 |
+
method="resource-templates/list", times=3
|
| 219 |
+
)
|
| 220 |
+
assert recording_middleware.assert_called(hook="on_message", times=1)
|
| 221 |
+
assert recording_middleware.assert_called(hook="on_request", times=1)
|
| 222 |
+
assert recording_middleware.assert_called(
|
| 223 |
+
hook="on_list_resource_templates", times=1
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
async def test_list_prompts(
|
| 227 |
+
self, mcp_server: FastMCP, recording_middleware: RecordingMiddleware
|
| 228 |
+
):
|
| 229 |
+
async with Client(mcp_server) as client:
|
| 230 |
+
await client.list_prompts()
|
| 231 |
+
|
| 232 |
+
assert recording_middleware.assert_called(times=3)
|
| 233 |
+
assert recording_middleware.assert_called(method="prompts/list", times=3)
|
| 234 |
+
assert recording_middleware.assert_called(hook="on_message", times=1)
|
| 235 |
+
assert recording_middleware.assert_called(hook="on_request", times=1)
|
| 236 |
+
assert recording_middleware.assert_called(hook="on_list_prompts", times=1)
|