mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-22 05:24:18 +02:00
Internal refactor of MCP handlers (#2005)
This commit is contained in:
parent
05dc0f4771
commit
91af3cd2ae
14 changed files with 247 additions and 192 deletions
|
|
@ -104,7 +104,7 @@ async def get_server_details():
|
|||
print(f" - Imported from news app: {news_resources}")
|
||||
|
||||
# Let's try to access resources using the prefixed URI
|
||||
weather_data = await app._mcp_read_resource(uri="weather://weather/forecast")
|
||||
weather_data = await app._read_resource_mcp(uri="weather://weather/forecast")
|
||||
print(f"\nWeather data from prefixed URI: {weather_data}")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ def get_example_data() -> dict:
|
|||
|
||||
|
||||
async def example_usage():
|
||||
result = await server._mcp_call_tool("get_example_data", {})
|
||||
result = await server._call_tool_mcp("get_example_data", {})
|
||||
print("Tool Result:")
|
||||
print(result)
|
||||
print("This is an example of using a custom serializer with FastMCP.")
|
||||
|
|
|
|||
|
|
@ -46,21 +46,23 @@ class PromptManager:
|
|||
"""Adds a mounted server as a source for prompts."""
|
||||
self._mounted_servers.append(server)
|
||||
|
||||
async def _load_prompts(self, *, via_server: bool = False) -> dict[str, Prompt]:
|
||||
async def _load_prompts(
|
||||
self, *, apply_filtering: bool = False
|
||||
) -> dict[str, Prompt]:
|
||||
"""
|
||||
The single, consolidated recursive method for fetching prompts. The 'via_server'
|
||||
The single, consolidated recursive method for fetching prompts. The 'apply_filtering'
|
||||
parameter determines the communication path.
|
||||
|
||||
- via_server=False: Manager-to-manager path for complete, unfiltered inventory
|
||||
- via_server=True: Server-to-server path for filtered MCP requests
|
||||
- apply_filtering=False: Manager-to-manager path for complete, unfiltered inventory
|
||||
- apply_filtering=True: Server-to-server path for filtered MCP requests
|
||||
"""
|
||||
all_prompts: dict[str, Prompt] = {}
|
||||
|
||||
for mounted in self._mounted_servers:
|
||||
try:
|
||||
if via_server:
|
||||
if apply_filtering:
|
||||
# Use the server-to-server filtered path
|
||||
child_results = await mounted.server._list_prompts()
|
||||
child_results = await mounted.server._list_prompts_middleware()
|
||||
else:
|
||||
# Use the manager-to-manager unfiltered path
|
||||
child_results = await mounted.server._prompt_manager.list_prompts()
|
||||
|
|
@ -104,13 +106,13 @@ class PromptManager:
|
|||
"""
|
||||
Gets the complete, unfiltered inventory of all prompts.
|
||||
"""
|
||||
return await self._load_prompts(via_server=False)
|
||||
return await self._load_prompts(apply_filtering=False)
|
||||
|
||||
async def list_prompts(self) -> list[Prompt]:
|
||||
"""
|
||||
Lists all prompts, applying protocol filtering.
|
||||
"""
|
||||
prompts_dict = await self._load_prompts(via_server=True)
|
||||
prompts_dict = await self._load_prompts(apply_filtering=True)
|
||||
return list(prompts_dict.values())
|
||||
|
||||
def add_prompt_from_fn(
|
||||
|
|
@ -196,7 +198,9 @@ class PromptManager:
|
|||
else:
|
||||
continue
|
||||
try:
|
||||
return await mounted.server._get_prompt(prompt_key, arguments)
|
||||
return await mounted.server._get_prompt_middleware(
|
||||
prompt_key, arguments
|
||||
)
|
||||
except NotFoundError:
|
||||
continue
|
||||
|
||||
|
|
|
|||
|
|
@ -63,27 +63,31 @@ class ResourceManager:
|
|||
|
||||
async def get_resources(self) -> dict[str, Resource]:
|
||||
"""Get all registered resources, keyed by URI."""
|
||||
return await self._load_resources(via_server=False)
|
||||
return await self._load_resources(apply_filtering=False)
|
||||
|
||||
async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
|
||||
"""Get all registered templates, keyed by URI template."""
|
||||
return await self._load_resource_templates(via_server=False)
|
||||
return await self._load_resource_templates(apply_filtering=False)
|
||||
|
||||
async def _load_resources(self, *, via_server: bool = False) -> dict[str, Resource]:
|
||||
async def _load_resources(
|
||||
self, *, apply_filtering: bool = False
|
||||
) -> dict[str, Resource]:
|
||||
"""
|
||||
The single, consolidated recursive method for fetching resources. The 'via_server'
|
||||
The single, consolidated recursive method for fetching resources. The 'apply_filtering'
|
||||
parameter determines the communication path.
|
||||
|
||||
- via_server=False: Manager-to-manager path for complete, unfiltered inventory
|
||||
- via_server=True: Server-to-server path for filtered MCP requests
|
||||
- apply_filtering=False: Manager-to-manager path for complete, unfiltered inventory
|
||||
- apply_filtering=True: Server-to-server path for filtered MCP requests
|
||||
"""
|
||||
all_resources: dict[str, Resource] = {}
|
||||
|
||||
for mounted in self._mounted_servers:
|
||||
try:
|
||||
if via_server:
|
||||
if apply_filtering:
|
||||
# Use the server-to-server filtered path
|
||||
child_resources_list = await mounted.server._list_resources()
|
||||
child_resources_list = (
|
||||
await mounted.server._list_resources_middleware()
|
||||
)
|
||||
child_resources = {
|
||||
resource.key: resource for resource in child_resources_list
|
||||
}
|
||||
|
|
@ -123,22 +127,24 @@ class ResourceManager:
|
|||
return all_resources
|
||||
|
||||
async def _load_resource_templates(
|
||||
self, *, via_server: bool = False
|
||||
self, *, apply_filtering: bool = False
|
||||
) -> dict[str, ResourceTemplate]:
|
||||
"""
|
||||
The single, consolidated recursive method for fetching templates. The 'via_server'
|
||||
The single, consolidated recursive method for fetching templates. The 'apply_filtering'
|
||||
parameter determines the communication path.
|
||||
|
||||
- via_server=False: Manager-to-manager path for complete, unfiltered inventory
|
||||
- via_server=True: Server-to-server path for filtered MCP requests
|
||||
- apply_filtering=False: Manager-to-manager path for complete, unfiltered inventory
|
||||
- apply_filtering=True: Server-to-server path for filtered MCP requests
|
||||
"""
|
||||
all_templates: dict[str, ResourceTemplate] = {}
|
||||
|
||||
for mounted in self._mounted_servers:
|
||||
try:
|
||||
if via_server:
|
||||
if apply_filtering:
|
||||
# Use the server-to-server filtered path
|
||||
child_templates = await mounted.server._list_resource_templates()
|
||||
child_templates = (
|
||||
await mounted.server._list_resource_templates_middleware()
|
||||
)
|
||||
else:
|
||||
# Use the manager-to-manager unfiltered path
|
||||
child_templates = (
|
||||
|
|
@ -179,14 +185,14 @@ class ResourceManager:
|
|||
"""
|
||||
Lists all resources, applying protocol filtering.
|
||||
"""
|
||||
resources_dict = await self._load_resources(via_server=True)
|
||||
resources_dict = await self._load_resources(apply_filtering=True)
|
||||
return list(resources_dict.values())
|
||||
|
||||
async def list_resource_templates(self) -> list[ResourceTemplate]:
|
||||
"""
|
||||
Lists all templates, applying protocol filtering.
|
||||
"""
|
||||
templates_dict = await self._load_resource_templates(via_server=True)
|
||||
templates_dict = await self._load_resource_templates(apply_filtering=True)
|
||||
return list(templates_dict.values())
|
||||
|
||||
def add_resource_or_template_from_fn(
|
||||
|
|
@ -492,7 +498,7 @@ class ResourceManager:
|
|||
continue
|
||||
|
||||
try:
|
||||
result = await mounted.server._read_resource(key)
|
||||
result = await mounted.server._read_resource_middleware(key)
|
||||
return result[0].content
|
||||
except NotFoundError:
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ class Context:
|
|||
"""
|
||||
if self.fastmcp is None:
|
||||
raise ValueError("Context is not available outside of a request")
|
||||
return await self.fastmcp._mcp_read_resource(uri)
|
||||
return await self.fastmcp._read_resource_mcp(uri)
|
||||
|
||||
async def log(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -386,13 +386,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
|
||||
def _setup_handlers(self) -> None:
|
||||
"""Set up core MCP protocol handlers."""
|
||||
self._mcp_server.list_tools()(self._mcp_list_tools)
|
||||
self._mcp_server.list_resources()(self._mcp_list_resources)
|
||||
self._mcp_server.list_resource_templates()(self._mcp_list_resource_templates)
|
||||
self._mcp_server.list_prompts()(self._mcp_list_prompts)
|
||||
self._mcp_server.call_tool()(self._mcp_call_tool)
|
||||
self._mcp_server.read_resource()(self._mcp_read_resource)
|
||||
self._mcp_server.get_prompt()(self._mcp_get_prompt)
|
||||
self._mcp_server.list_tools()(self._list_tools_mcp)
|
||||
self._mcp_server.list_resources()(self._list_resources_mcp)
|
||||
self._mcp_server.list_resource_templates()(self._list_resource_templates_mcp)
|
||||
self._mcp_server.list_prompts()(self._list_prompts_mcp)
|
||||
self._mcp_server.call_tool()(self._call_tool_mcp)
|
||||
self._mcp_server.read_resource()(self._read_resource_mcp)
|
||||
self._mcp_server.get_prompt()(self._get_prompt_mcp)
|
||||
|
||||
async def _apply_middleware(
|
||||
self,
|
||||
|
|
@ -519,11 +519,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
|
||||
return routes
|
||||
|
||||
async def _mcp_list_tools(self) -> list[MCPTool]:
|
||||
async def _list_tools_mcp(self) -> list[MCPTool]:
|
||||
"""
|
||||
List all available tools, in the format expected by the low-level MCP
|
||||
server.
|
||||
"""
|
||||
logger.debug(f"[{self.name}] Handler called: list_tools")
|
||||
|
||||
async with fastmcp.server.context.Context(fastmcp=self):
|
||||
tools = await self._list_tools()
|
||||
tools = await self._list_tools_middleware()
|
||||
return [
|
||||
tool.to_mcp_tool(
|
||||
name=tool.key,
|
||||
|
|
@ -532,24 +536,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
for tool in tools
|
||||
]
|
||||
|
||||
async def _list_tools(self) -> list[Tool]:
|
||||
async def _list_tools_middleware(self) -> list[Tool]:
|
||||
"""
|
||||
List all available tools, in the format expected by the low-level MCP
|
||||
server.
|
||||
List all available tools, applying MCP middleware.
|
||||
"""
|
||||
|
||||
async def _handler(
|
||||
context: MiddlewareContext[mcp.types.ListToolsRequest],
|
||||
) -> list[Tool]:
|
||||
tools = await self._tool_manager.list_tools() # type: ignore[reportPrivateUsage]
|
||||
|
||||
mcp_tools: list[Tool] = []
|
||||
for tool in tools:
|
||||
if self._should_enable_component(tool):
|
||||
mcp_tools.append(tool)
|
||||
|
||||
return mcp_tools
|
||||
|
||||
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
||||
# Create the middleware context.
|
||||
mw_context = MiddlewareContext(
|
||||
|
|
@ -561,13 +552,33 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
)
|
||||
|
||||
# Apply the middleware chain.
|
||||
return await self._apply_middleware(mw_context, _handler)
|
||||
return await self._apply_middleware(mw_context, self._list_tools)
|
||||
|
||||
async def _mcp_list_resources(self) -> list[MCPResource]:
|
||||
async def _list_tools(
|
||||
self,
|
||||
context: MiddlewareContext[mcp.types.ListToolsRequest],
|
||||
) -> list[Tool]:
|
||||
"""
|
||||
List all available tools
|
||||
"""
|
||||
tools = await self._tool_manager.list_tools() # type: ignore[reportPrivateUsage]
|
||||
|
||||
mcp_tools: list[Tool] = []
|
||||
for tool in tools:
|
||||
if self._should_enable_component(tool):
|
||||
mcp_tools.append(tool)
|
||||
|
||||
return mcp_tools
|
||||
|
||||
async def _list_resources_mcp(self) -> list[MCPResource]:
|
||||
"""
|
||||
List all available resources, in the format expected by the low-level MCP
|
||||
server.
|
||||
"""
|
||||
logger.debug(f"[{self.name}] Handler called: list_resources")
|
||||
|
||||
async with fastmcp.server.context.Context(fastmcp=self):
|
||||
resources = await self._list_resources()
|
||||
resources = await self._list_resources_middleware()
|
||||
return [
|
||||
resource.to_mcp_resource(
|
||||
uri=resource.key,
|
||||
|
|
@ -576,25 +587,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
for resource in resources
|
||||
]
|
||||
|
||||
async def _list_resources(self) -> list[Resource]:
|
||||
async def _list_resources_middleware(self) -> list[Resource]:
|
||||
"""
|
||||
List all available resources, in the format expected by the low-level MCP
|
||||
server.
|
||||
|
||||
List all available resources, applying MCP middleware.
|
||||
"""
|
||||
|
||||
async def _handler(
|
||||
context: MiddlewareContext[dict[str, Any]],
|
||||
) -> list[Resource]:
|
||||
resources = await self._resource_manager.list_resources() # type: ignore[reportPrivateUsage]
|
||||
|
||||
mcp_resources: list[Resource] = []
|
||||
for resource in resources:
|
||||
if self._should_enable_component(resource):
|
||||
mcp_resources.append(resource)
|
||||
|
||||
return mcp_resources
|
||||
|
||||
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
||||
# Create the middleware context.
|
||||
mw_context = MiddlewareContext(
|
||||
|
|
@ -606,13 +603,33 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
)
|
||||
|
||||
# Apply the middleware chain.
|
||||
return await self._apply_middleware(mw_context, _handler)
|
||||
return await self._apply_middleware(mw_context, self._list_resources)
|
||||
|
||||
async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
|
||||
async def _list_resources(
|
||||
self,
|
||||
context: MiddlewareContext[dict[str, Any]],
|
||||
) -> list[Resource]:
|
||||
"""
|
||||
List all available resources
|
||||
"""
|
||||
resources = await self._resource_manager.list_resources() # type: ignore[reportPrivateUsage]
|
||||
|
||||
mcp_resources: list[Resource] = []
|
||||
for resource in resources:
|
||||
if self._should_enable_component(resource):
|
||||
mcp_resources.append(resource)
|
||||
|
||||
return mcp_resources
|
||||
|
||||
async def _list_resource_templates_mcp(self) -> list[MCPResourceTemplate]:
|
||||
"""
|
||||
List all available resource templates, in the format expected by the low-level MCP
|
||||
server.
|
||||
"""
|
||||
logger.debug(f"[{self.name}] Handler called: list_resource_templates")
|
||||
|
||||
async with fastmcp.server.context.Context(fastmcp=self):
|
||||
templates = await self._list_resource_templates()
|
||||
templates = await self._list_resource_templates_middleware()
|
||||
return [
|
||||
template.to_mcp_template(
|
||||
uriTemplate=template.key,
|
||||
|
|
@ -621,25 +638,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
for template in templates
|
||||
]
|
||||
|
||||
async def _list_resource_templates(self) -> list[ResourceTemplate]:
|
||||
async def _list_resource_templates_middleware(self) -> list[ResourceTemplate]:
|
||||
"""
|
||||
List all available resource templates, in the format expected by the low-level MCP
|
||||
server.
|
||||
List all available resource templates, applying MCP middleware.
|
||||
|
||||
"""
|
||||
|
||||
async def _handler(
|
||||
context: MiddlewareContext[dict[str, Any]],
|
||||
) -> list[ResourceTemplate]:
|
||||
templates = await self._resource_manager.list_resource_templates()
|
||||
|
||||
mcp_templates: list[ResourceTemplate] = []
|
||||
for template in templates:
|
||||
if self._should_enable_component(template):
|
||||
mcp_templates.append(template)
|
||||
|
||||
return mcp_templates
|
||||
|
||||
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
||||
# Create the middleware context.
|
||||
mw_context = MiddlewareContext(
|
||||
|
|
@ -651,13 +655,35 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
)
|
||||
|
||||
# Apply the middleware chain.
|
||||
return await self._apply_middleware(mw_context, _handler)
|
||||
return await self._apply_middleware(
|
||||
mw_context, self._list_resource_templates
|
||||
)
|
||||
|
||||
async def _mcp_list_prompts(self) -> list[MCPPrompt]:
|
||||
async def _list_resource_templates(
|
||||
self,
|
||||
context: MiddlewareContext[dict[str, Any]],
|
||||
) -> list[ResourceTemplate]:
|
||||
"""
|
||||
List all available resource templates
|
||||
"""
|
||||
templates = await self._resource_manager.list_resource_templates() # type: ignore[reportPrivateUsage]
|
||||
|
||||
mcp_templates: list[ResourceTemplate] = []
|
||||
for template in templates:
|
||||
if self._should_enable_component(template):
|
||||
mcp_templates.append(template)
|
||||
|
||||
return mcp_templates
|
||||
|
||||
async def _list_prompts_mcp(self) -> list[MCPPrompt]:
|
||||
"""
|
||||
List all available prompts, in the format expected by the low-level MCP
|
||||
server.
|
||||
"""
|
||||
logger.debug(f"[{self.name}] Handler called: list_prompts")
|
||||
|
||||
async with fastmcp.server.context.Context(fastmcp=self):
|
||||
prompts = await self._list_prompts()
|
||||
prompts = await self._list_prompts_middleware()
|
||||
return [
|
||||
prompt.to_mcp_prompt(
|
||||
name=prompt.key,
|
||||
|
|
@ -666,25 +692,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
for prompt in prompts
|
||||
]
|
||||
|
||||
async def _list_prompts(self) -> list[Prompt]:
|
||||
async def _list_prompts_middleware(self) -> list[Prompt]:
|
||||
"""
|
||||
List all available prompts, in the format expected by the low-level MCP
|
||||
server.
|
||||
List all available prompts, applying MCP middleware.
|
||||
|
||||
"""
|
||||
|
||||
async def _handler(
|
||||
context: MiddlewareContext[mcp.types.ListPromptsRequest],
|
||||
) -> list[Prompt]:
|
||||
prompts = await self._prompt_manager.list_prompts() # type: ignore[reportPrivateUsage]
|
||||
|
||||
mcp_prompts: list[Prompt] = []
|
||||
for prompt in prompts:
|
||||
if self._should_enable_component(prompt):
|
||||
mcp_prompts.append(prompt)
|
||||
|
||||
return mcp_prompts
|
||||
|
||||
async with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
|
||||
# Create the middleware context.
|
||||
mw_context = MiddlewareContext(
|
||||
|
|
@ -696,9 +709,25 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
)
|
||||
|
||||
# Apply the middleware chain.
|
||||
return await self._apply_middleware(mw_context, _handler)
|
||||
return await self._apply_middleware(mw_context, self._list_prompts)
|
||||
|
||||
async def _mcp_call_tool(
|
||||
async def _list_prompts(
|
||||
self,
|
||||
context: MiddlewareContext[mcp.types.ListPromptsRequest],
|
||||
) -> list[Prompt]:
|
||||
"""
|
||||
List all available prompts
|
||||
"""
|
||||
prompts = await self._prompt_manager.list_prompts() # type: ignore[reportPrivateUsage]
|
||||
|
||||
mcp_prompts: list[Prompt] = []
|
||||
for prompt in prompts:
|
||||
if self._should_enable_component(prompt):
|
||||
mcp_prompts.append(prompt)
|
||||
|
||||
return mcp_prompts
|
||||
|
||||
async def _call_tool_mcp(
|
||||
self, key: str, arguments: dict[str, Any]
|
||||
) -> list[ContentBlock] | tuple[list[ContentBlock], dict[str, Any]]:
|
||||
"""
|
||||
|
|
@ -719,29 +748,22 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
|
||||
async with fastmcp.server.context.Context(fastmcp=self):
|
||||
try:
|
||||
result = await self._call_tool(key, arguments)
|
||||
result = await self._call_tool_middleware(key, arguments)
|
||||
return result.to_mcp_result()
|
||||
except DisabledError:
|
||||
raise NotFoundError(f"Unknown tool: {key}")
|
||||
except NotFoundError:
|
||||
raise NotFoundError(f"Unknown tool: {key}")
|
||||
|
||||
async def _call_tool(self, key: str, arguments: dict[str, Any]) -> ToolResult:
|
||||
async def _call_tool_middleware(
|
||||
self,
|
||||
key: str,
|
||||
arguments: dict[str, Any],
|
||||
) -> ToolResult:
|
||||
"""
|
||||
Applies this server's middleware and delegates the filtered call to the manager.
|
||||
"""
|
||||
|
||||
async def _handler(
|
||||
context: MiddlewareContext[mcp.types.CallToolRequestParams],
|
||||
) -> ToolResult:
|
||||
tool = await self._tool_manager.get_tool(context.message.name)
|
||||
if not self._should_enable_component(tool):
|
||||
raise NotFoundError(f"Unknown tool: {context.message.name!r}")
|
||||
|
||||
return await self._tool_manager.call_tool(
|
||||
key=context.message.name, arguments=context.message.arguments or {}
|
||||
)
|
||||
|
||||
mw_context = MiddlewareContext[CallToolRequestParams](
|
||||
message=mcp.types.CallToolRequestParams(name=key, arguments=arguments),
|
||||
source="client",
|
||||
|
|
@ -749,9 +771,24 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
method="tools/call",
|
||||
fastmcp_context=fastmcp.server.dependencies.get_context(),
|
||||
)
|
||||
return await self._apply_middleware(mw_context, _handler)
|
||||
return await self._apply_middleware(mw_context, self._call_tool)
|
||||
|
||||
async def _mcp_read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
|
||||
async def _call_tool(
|
||||
self,
|
||||
context: MiddlewareContext[mcp.types.CallToolRequestParams],
|
||||
) -> ToolResult:
|
||||
"""
|
||||
Call a tool
|
||||
"""
|
||||
tool = await self._tool_manager.get_tool(context.message.name)
|
||||
if not self._should_enable_component(tool):
|
||||
raise NotFoundError(f"Unknown tool: {context.message.name!r}")
|
||||
|
||||
return await self._tool_manager.call_tool(
|
||||
key=context.message.name, arguments=context.message.arguments or {}
|
||||
)
|
||||
|
||||
async def _read_resource_mcp(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
|
||||
"""
|
||||
Handle MCP 'readResource' requests.
|
||||
|
||||
|
|
@ -761,7 +798,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
|
||||
async with fastmcp.server.context.Context(fastmcp=self):
|
||||
try:
|
||||
return await self._read_resource(uri)
|
||||
return await self._read_resource_middleware(uri)
|
||||
except DisabledError:
|
||||
# convert to NotFoundError to avoid leaking resource presence
|
||||
raise NotFoundError(f"Unknown resource: {str(uri)!r}")
|
||||
|
|
@ -769,26 +806,14 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
# standardize NotFound message
|
||||
raise NotFoundError(f"Unknown resource: {str(uri)!r}")
|
||||
|
||||
async def _read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
|
||||
async def _read_resource_middleware(
|
||||
self,
|
||||
uri: AnyUrl | str,
|
||||
) -> list[ReadResourceContents]:
|
||||
"""
|
||||
Applies this server's middleware and delegates the filtered call to the manager.
|
||||
"""
|
||||
|
||||
async def _handler(
|
||||
context: MiddlewareContext[mcp.types.ReadResourceRequestParams],
|
||||
) -> list[ReadResourceContents]:
|
||||
resource = await self._resource_manager.get_resource(context.message.uri)
|
||||
if not self._should_enable_component(resource):
|
||||
raise NotFoundError(f"Unknown resource: {str(context.message.uri)!r}")
|
||||
|
||||
content = await self._resource_manager.read_resource(context.message.uri)
|
||||
return [
|
||||
ReadResourceContents(
|
||||
content=content,
|
||||
mime_type=resource.mime_type,
|
||||
)
|
||||
]
|
||||
|
||||
# Convert string URI to AnyUrl if needed
|
||||
if isinstance(uri, str):
|
||||
uri_param = AnyUrl(uri)
|
||||
|
|
@ -802,9 +827,28 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
method="resources/read",
|
||||
fastmcp_context=fastmcp.server.dependencies.get_context(),
|
||||
)
|
||||
return await self._apply_middleware(mw_context, _handler)
|
||||
return await self._apply_middleware(mw_context, self._read_resource)
|
||||
|
||||
async def _mcp_get_prompt(
|
||||
async def _read_resource(
|
||||
self,
|
||||
context: MiddlewareContext[mcp.types.ReadResourceRequestParams],
|
||||
) -> list[ReadResourceContents]:
|
||||
"""
|
||||
Read a resource
|
||||
"""
|
||||
resource = await self._resource_manager.get_resource(context.message.uri)
|
||||
if not self._should_enable_component(resource):
|
||||
raise NotFoundError(f"Unknown resource: {str(context.message.uri)!r}")
|
||||
|
||||
content = await self._resource_manager.read_resource(context.message.uri)
|
||||
return [
|
||||
ReadResourceContents(
|
||||
content=content,
|
||||
mime_type=resource.mime_type,
|
||||
)
|
||||
]
|
||||
|
||||
async def _get_prompt_mcp(
|
||||
self, name: str, arguments: dict[str, Any] | None = None
|
||||
) -> GetPromptResult:
|
||||
"""
|
||||
|
|
@ -820,7 +864,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
|
||||
async with fastmcp.server.context.Context(fastmcp=self):
|
||||
try:
|
||||
return await self._get_prompt(name, arguments)
|
||||
return await self._get_prompt_middleware(name, arguments)
|
||||
except DisabledError:
|
||||
# convert to NotFoundError to avoid leaking prompt presence
|
||||
raise NotFoundError(f"Unknown prompt: {name}")
|
||||
|
|
@ -828,24 +872,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
# standardize NotFound message
|
||||
raise NotFoundError(f"Unknown prompt: {name}")
|
||||
|
||||
async def _get_prompt(
|
||||
async def _get_prompt_middleware(
|
||||
self, name: str, arguments: dict[str, Any] | None = None
|
||||
) -> GetPromptResult:
|
||||
"""
|
||||
Applies this server's middleware and delegates the filtered call to the manager.
|
||||
"""
|
||||
|
||||
async def _handler(
|
||||
context: MiddlewareContext[mcp.types.GetPromptRequestParams],
|
||||
) -> GetPromptResult:
|
||||
prompt = await self._prompt_manager.get_prompt(context.message.name)
|
||||
if not self._should_enable_component(prompt):
|
||||
raise NotFoundError(f"Unknown prompt: {context.message.name!r}")
|
||||
|
||||
return await self._prompt_manager.render_prompt(
|
||||
name=context.message.name, arguments=context.message.arguments
|
||||
)
|
||||
|
||||
mw_context = MiddlewareContext(
|
||||
message=mcp.types.GetPromptRequestParams(name=name, arguments=arguments),
|
||||
source="client",
|
||||
|
|
@ -853,7 +886,19 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
method="prompts/get",
|
||||
fastmcp_context=fastmcp.server.dependencies.get_context(),
|
||||
)
|
||||
return await self._apply_middleware(mw_context, _handler)
|
||||
return await self._apply_middleware(mw_context, self._get_prompt)
|
||||
|
||||
async def _get_prompt(
|
||||
self,
|
||||
context: MiddlewareContext[mcp.types.GetPromptRequestParams],
|
||||
) -> GetPromptResult:
|
||||
prompt = await self._prompt_manager.get_prompt(context.message.name)
|
||||
if not self._should_enable_component(prompt):
|
||||
raise NotFoundError(f"Unknown prompt: {context.message.name!r}")
|
||||
|
||||
return await self._prompt_manager.render_prompt(
|
||||
name=context.message.name, arguments=context.message.arguments
|
||||
)
|
||||
|
||||
def add_tool(self, tool: Tool) -> Tool:
|
||||
"""Add a tool to the server.
|
||||
|
|
|
|||
|
|
@ -52,21 +52,21 @@ class ToolManager:
|
|||
"""Adds a mounted server as a source for tools."""
|
||||
self._mounted_servers.append(server)
|
||||
|
||||
async def _load_tools(self, *, via_server: bool = False) -> dict[str, Tool]:
|
||||
async def _load_tools(self, *, apply_filtering: bool = False) -> dict[str, Tool]:
|
||||
"""
|
||||
The single, consolidated recursive method for fetching tools. The 'via_server'
|
||||
The single, consolidated recursive method for fetching tools. The 'apply_filtering'
|
||||
parameter determines the communication path.
|
||||
|
||||
- via_server=False: Manager-to-manager path for complete, unfiltered inventory
|
||||
- via_server=True: Server-to-server path for filtered MCP requests
|
||||
- apply_filtering=False: Manager-to-manager path for complete, unfiltered inventory
|
||||
- apply_filtering=True: Server-to-server path for filtered MCP requests
|
||||
"""
|
||||
all_tools: dict[str, Tool] = {}
|
||||
|
||||
for mounted in self._mounted_servers:
|
||||
try:
|
||||
if via_server:
|
||||
if apply_filtering:
|
||||
# Use the server-to-server filtered path
|
||||
child_results = await mounted.server._list_tools()
|
||||
child_results = await mounted.server._list_tools_middleware()
|
||||
else:
|
||||
# Use the manager-to-manager unfiltered path
|
||||
child_results = await mounted.server._tool_manager.list_tools()
|
||||
|
|
@ -116,13 +116,13 @@ class ToolManager:
|
|||
"""
|
||||
Gets the complete, unfiltered inventory of all tools.
|
||||
"""
|
||||
return await self._load_tools(via_server=False)
|
||||
return await self._load_tools(apply_filtering=False)
|
||||
|
||||
async def list_tools(self) -> list[Tool]:
|
||||
"""
|
||||
Lists all tools, applying protocol filtering.
|
||||
"""
|
||||
tools_dict = await self._load_tools(via_server=True)
|
||||
tools_dict = await self._load_tools(apply_filtering=True)
|
||||
return list(tools_dict.values())
|
||||
|
||||
@property
|
||||
|
|
@ -247,7 +247,7 @@ class ToolManager:
|
|||
else:
|
||||
continue
|
||||
try:
|
||||
return await mounted.server._call_tool(tool_key, arguments)
|
||||
return await mounted.server._call_tool_middleware(tool_key, arguments)
|
||||
except NotFoundError:
|
||||
continue
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class TestCustomRoutes:
|
|||
|
||||
return server
|
||||
|
||||
def test_custom_routes_via_server_http_app(self, server_with_custom_route):
|
||||
def test_custom_routes_apply_filtering_http_app(self, server_with_custom_route):
|
||||
"""Test that custom routes are included when using server.http_app()."""
|
||||
# Get the app via server.http_app()
|
||||
app = server_with_custom_route.http_app()
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ async def test_integration_array_path_parameter(array_path_spec, mock_client):
|
|||
mcp = FastMCP.from_openapi(array_path_spec, client=mock_client)
|
||||
|
||||
# Call the tool with a single value
|
||||
await mcp._mcp_call_tool("test_operation", {"days": ["monday"]})
|
||||
await mcp._call_tool_mcp("test_operation", {"days": ["monday"]})
|
||||
|
||||
# Check the request was made correctly
|
||||
mock_client.request.assert_called_with(
|
||||
|
|
@ -177,7 +177,7 @@ async def test_integration_array_path_parameter(array_path_spec, mock_client):
|
|||
mock_client.request.reset_mock()
|
||||
|
||||
# Call the tool with multiple values
|
||||
await mcp._mcp_call_tool("test_operation", {"days": ["monday", "tuesday"]})
|
||||
await mcp._call_tool_mcp("test_operation", {"days": ["monday", "tuesday"]})
|
||||
|
||||
# Check the request was made correctly
|
||||
mock_client.request.assert_called_with(
|
||||
|
|
|
|||
|
|
@ -173,15 +173,15 @@ class TestTools:
|
|||
|
||||
async def test_list_tools_same_as_original(self, fastmcp_server, proxy_server):
|
||||
assert (
|
||||
await proxy_server._mcp_list_tools()
|
||||
== await fastmcp_server._mcp_list_tools()
|
||||
await proxy_server._list_tools_mcp()
|
||||
== await fastmcp_server._list_tools_mcp()
|
||||
)
|
||||
|
||||
async def test_call_tool_result_same_as_original(
|
||||
self, fastmcp_server: FastMCP, proxy_server: FastMCPProxy
|
||||
):
|
||||
result = await fastmcp_server._mcp_call_tool("greet", {"name": "Alice"})
|
||||
proxy_result = await proxy_server._mcp_call_tool("greet", {"name": "Alice"})
|
||||
result = await fastmcp_server._call_tool_mcp("greet", {"name": "Alice"})
|
||||
proxy_result = await proxy_server._call_tool_mcp("greet", {"name": "Alice"})
|
||||
|
||||
assert result == proxy_result
|
||||
|
||||
|
|
@ -267,8 +267,8 @@ class TestResources:
|
|||
|
||||
async def test_list_resources_same_as_original(self, fastmcp_server, proxy_server):
|
||||
assert (
|
||||
await proxy_server._mcp_list_resources()
|
||||
== await fastmcp_server._mcp_list_resources()
|
||||
await proxy_server._list_resources_mcp()
|
||||
== await fastmcp_server._list_resources_mcp()
|
||||
)
|
||||
|
||||
async def test_read_resource(self, proxy_server: FastMCPProxy):
|
||||
|
|
@ -367,8 +367,8 @@ class TestResourceTemplates:
|
|||
async def test_list_resource_templates_same_as_original(
|
||||
self, fastmcp_server, proxy_server
|
||||
):
|
||||
result = await fastmcp_server._mcp_list_resource_templates()
|
||||
proxy_result = await proxy_server._mcp_list_resource_templates()
|
||||
result = await fastmcp_server._list_resource_templates_mcp()
|
||||
proxy_result = await proxy_server._list_resource_templates_mcp()
|
||||
assert proxy_result == result
|
||||
|
||||
@pytest.mark.parametrize("id", [1, 2, 3])
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ def tools(mcp: FastMCP, test_dir: Path) -> FastMCP:
|
|||
|
||||
|
||||
async def test_list_resources(mcp: FastMCP):
|
||||
resources = await mcp._mcp_list_resources()
|
||||
resources = await mcp._list_resources_mcp()
|
||||
assert len(resources) == 4
|
||||
|
||||
assert [str(r.uri) for r in resources] == [
|
||||
|
|
@ -86,7 +86,7 @@ async def test_list_resources(mcp: FastMCP):
|
|||
|
||||
|
||||
async def test_read_resource_dir(mcp: FastMCP):
|
||||
res_iter = await mcp._mcp_read_resource("dir://test_dir")
|
||||
res_iter = await mcp._read_resource_mcp("dir://test_dir")
|
||||
res_list = list(res_iter)
|
||||
assert len(res_list) == 1
|
||||
res = res_list[0]
|
||||
|
|
@ -102,7 +102,7 @@ async def test_read_resource_dir(mcp: FastMCP):
|
|||
|
||||
|
||||
async def test_read_resource_file(mcp: FastMCP):
|
||||
res_iter = await mcp._mcp_read_resource("file://test_dir/example.py")
|
||||
res_iter = await mcp._read_resource_mcp("file://test_dir/example.py")
|
||||
res_list = list(res_iter)
|
||||
assert len(res_list) == 1
|
||||
res = res_list[0]
|
||||
|
|
@ -110,17 +110,17 @@ async def test_read_resource_file(mcp: FastMCP):
|
|||
|
||||
|
||||
async def test_delete_file(mcp: FastMCP, test_dir: Path):
|
||||
await mcp._mcp_call_tool(
|
||||
await mcp._call_tool_mcp(
|
||||
"delete_file", arguments=dict(path=str(test_dir / "example.py"))
|
||||
)
|
||||
assert not (test_dir / "example.py").exists()
|
||||
|
||||
|
||||
async def test_delete_file_and_check_resources(mcp: FastMCP, test_dir: Path):
|
||||
await mcp._mcp_call_tool(
|
||||
await mcp._call_tool_mcp(
|
||||
"delete_file", arguments=dict(path=str(test_dir / "example.py"))
|
||||
)
|
||||
res_iter = await mcp._mcp_read_resource("file://test_dir/example.py")
|
||||
res_iter = await mcp._read_resource_mcp("file://test_dir/example.py")
|
||||
res_list = list(res_iter)
|
||||
assert len(res_list) == 1
|
||||
res = res_list[0]
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ class TestTools:
|
|||
def fn(x: int) -> int:
|
||||
return x + 1
|
||||
|
||||
mcp_tools = await mcp._mcp_list_tools()
|
||||
mcp_tools = await mcp._list_tools_mcp()
|
||||
assert len(mcp_tools) == 1
|
||||
assert mcp_tools[0].name == "fn"
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ class TestTools:
|
|||
def fn(x: int) -> int:
|
||||
return x + 1
|
||||
|
||||
mcp_tools = await mcp._mcp_list_tools()
|
||||
mcp_tools = await mcp._list_tools_mcp()
|
||||
assert len(mcp_tools) == 1
|
||||
assert mcp_tools[0].name == "custom_name"
|
||||
|
||||
|
|
@ -110,7 +110,7 @@ class TestTools:
|
|||
assert "adder" not in mcp_tools
|
||||
|
||||
with pytest.raises(NotFoundError, match="Unknown tool: adder"):
|
||||
await mcp._mcp_call_tool("adder", {"a": 1, "b": 2})
|
||||
await mcp._call_tool_mcp("adder", {"a": 1, "b": 2})
|
||||
|
||||
async def test_add_tool_at_init(self):
|
||||
def f(x: int) -> int:
|
||||
|
|
@ -136,7 +136,7 @@ class TestToolDecorator:
|
|||
mcp = FastMCP()
|
||||
|
||||
with pytest.raises(NotFoundError, match="Unknown tool: add"):
|
||||
await mcp._mcp_call_tool("add", {"x": 1, "y": 2})
|
||||
await mcp._call_tool_mcp("add", {"x": 1, "y": 2})
|
||||
|
||||
async def test_tool_decorator(self):
|
||||
mcp = FastMCP()
|
||||
|
|
@ -185,7 +185,7 @@ class TestToolDecorator:
|
|||
def add(x: int, y: int) -> int:
|
||||
return x + y
|
||||
|
||||
tools = await mcp._mcp_list_tools()
|
||||
tools = await mcp._list_tools_mcp()
|
||||
assert len(tools) == 1
|
||||
tool = tools[0]
|
||||
assert tool.description == "Add two numbers"
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ async def test_tool_annotations_in_mcp_protocol():
|
|||
return message
|
||||
|
||||
# Check via MCP protocol
|
||||
mcp_tools = await mcp._mcp_list_tools()
|
||||
mcp_tools = await mcp._list_tools_mcp()
|
||||
assert len(mcp_tools) == 1
|
||||
assert mcp_tools[0].annotations is not None
|
||||
assert mcp_tools[0].annotations.title == "Echo Tool"
|
||||
|
|
|
|||
|
|
@ -29,14 +29,14 @@ async def test_transformed_tool_filtering():
|
|||
"""Echo back the message provided."""
|
||||
return message
|
||||
|
||||
tools = list(await mcp._list_tools())
|
||||
tools = list(await mcp._list_tools_middleware())
|
||||
assert len(tools) == 0
|
||||
|
||||
mcp.add_tool_transformation(
|
||||
"echo", ToolTransformConfig(name="echo_transformed", tags={"enabled_tools"})
|
||||
)
|
||||
|
||||
tools = list(await mcp._list_tools())
|
||||
tools = list(await mcp._list_tools_middleware())
|
||||
assert len(tools) == 1
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue