Jeremiah Lowin commited on
Commit
e97c2b8
·
1 Parent(s): ba3b96f

Merge main

Browse files
src/fastmcp/server/{middleware/middleware.py → middleware.py} RENAMED
@@ -17,6 +17,11 @@ from typing import (
17
 
18
  import mcp.types as mt
19
 
 
 
 
 
 
20
  if TYPE_CHECKING:
21
  from fastmcp.server.context import Context
22
 
@@ -47,6 +52,32 @@ ServerResultT = TypeVar(
47
  )
48
 
49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  @runtime_checkable
51
  class ServerResultProtocol(Protocol[ServerResultT]):
52
  root: ServerResultT
@@ -95,29 +126,29 @@ class MCPMiddleware:
95
  ) -> Any:
96
  """Main entry point that orchestrates the pipeline."""
97
  handler_chain = await self._dispatch_handler(
98
- context.message,
99
  call_next=call_next,
100
  )
101
  return await handler_chain(context)
102
 
103
  async def _dispatch_handler(
104
- self, message: Any, call_next: CallNext[Any, Any]
105
  ) -> CallNext[Any, Any]:
106
  """Builds a chain of handlers for a given message."""
107
  handler = call_next
108
 
109
- match message:
110
- case mt.CallToolRequest():
111
  handler = partial(self.on_call_tool, call_next=handler)
112
- case mt.ReadResourceRequest():
113
  handler = partial(self.on_read_resource, call_next=handler)
114
- case mt.GetPromptRequest():
115
  handler = partial(self.on_get_prompt, call_next=handler)
116
 
117
- match message:
118
- case mt.Request():
119
  handler = partial(self.on_request, call_next=handler)
120
- case mt.Notification():
121
  handler = partial(self.on_notification, call_next=handler)
122
 
123
  handler = partial(self.on_message, call_next=handler)
@@ -147,28 +178,28 @@ class MCPMiddleware:
147
 
148
  async def on_call_tool(
149
  self,
150
- context: MiddlewareContext[mt.CallToolRequest],
151
- call_next: CallNext[mt.CallToolRequest, mt.CallToolResult],
152
  ) -> mt.CallToolResult:
153
  return await call_next(context)
154
 
155
  async def on_read_resource(
156
  self,
157
- context: MiddlewareContext[mt.ReadResourceRequest],
158
- call_next: CallNext[mt.ReadResourceRequest, mt.ReadResourceResult],
159
  ) -> mt.ReadResourceResult:
160
  return await call_next(context)
161
 
162
  async def on_get_prompt(
163
  self,
164
- context: MiddlewareContext[mt.GetPromptRequest],
165
- call_next: CallNext[mt.GetPromptRequest, mt.GetPromptResult],
166
  ) -> mt.GetPromptResult:
167
  return await call_next(context)
168
 
169
  async def on_list_tools(
170
  self,
171
  context: MiddlewareContext[mt.ListToolsRequest],
172
- call_next: CallNext[mt.ListToolsRequest, mt.ListToolsResult],
173
- ) -> mt.ListToolsResult:
174
  return await call_next(context)
 
17
 
18
  import mcp.types as mt
19
 
20
+ from fastmcp.prompts.prompt import Prompt
21
+ from fastmcp.resources.resource import Resource
22
+ from fastmcp.resources.template import ResourceTemplate
23
+ from fastmcp.tools.tool import Tool
24
+
25
  if TYPE_CHECKING:
26
  from fastmcp.server.context import Context
27
 
 
52
  )
53
 
54
 
55
+ @dataclass(kw_only=True)
56
+ class CallToolResult:
57
+ content: list[mt.Content]
58
+ isError: bool = False
59
+
60
+
61
+ @dataclass(kw_only=True)
62
+ class ListToolsResult:
63
+ tools: dict[str, Tool]
64
+
65
+
66
+ @dataclass(kw_only=True)
67
+ class ListResourcesResult:
68
+ resources: list[Resource]
69
+
70
+
71
+ @dataclass(kw_only=True)
72
+ class ListResourceTemplatesResult:
73
+ resource_templates: list[ResourceTemplate]
74
+
75
+
76
+ @dataclass(kw_only=True)
77
+ class ListPromptsResult:
78
+ prompts: list[Prompt]
79
+
80
+
81
  @runtime_checkable
82
  class ServerResultProtocol(Protocol[ServerResultT]):
83
  root: ServerResultT
 
126
  ) -> Any:
127
  """Main entry point that orchestrates the pipeline."""
128
  handler_chain = await self._dispatch_handler(
129
+ context,
130
  call_next=call_next,
131
  )
132
  return await handler_chain(context)
133
 
134
  async def _dispatch_handler(
135
+ self, context: MiddlewareContext[Any], call_next: CallNext[Any, Any]
136
  ) -> CallNext[Any, Any]:
137
  """Builds a chain of handlers for a given message."""
138
  handler = call_next
139
 
140
+ match context.method:
141
+ case "tools/call":
142
  handler = partial(self.on_call_tool, call_next=handler)
143
+ case "resources/read":
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":
150
  handler = partial(self.on_request, call_next=handler)
151
+ case "notification":
152
  handler = partial(self.on_notification, call_next=handler)
153
 
154
  handler = partial(self.on_message, call_next=handler)
 
178
 
179
  async def on_call_tool(
180
  self,
181
+ context: MiddlewareContext[mt.CallToolRequestParams],
182
+ call_next: CallNext[mt.CallToolRequestParams, mt.CallToolResult],
183
  ) -> mt.CallToolResult:
184
  return await call_next(context)
185
 
186
  async def on_read_resource(
187
  self,
188
+ context: MiddlewareContext[mt.ReadResourceRequestParams],
189
+ call_next: CallNext[mt.ReadResourceRequestParams, mt.ReadResourceResult],
190
  ) -> mt.ReadResourceResult:
191
  return await call_next(context)
192
 
193
  async def on_get_prompt(
194
  self,
195
+ context: MiddlewareContext[mt.GetPromptRequestParams],
196
+ call_next: CallNext[mt.GetPromptRequestParams, mt.GetPromptResult],
197
  ) -> mt.GetPromptResult:
198
  return await call_next(context)
199
 
200
  async def on_list_tools(
201
  self,
202
  context: MiddlewareContext[mt.ListToolsRequest],
203
+ call_next: CallNext[mt.ListToolsRequest, ListToolsResult],
204
+ ) -> ListToolsResult:
205
  return await call_next(context)
src/fastmcp/server/middleware/__init__.py DELETED
File without changes
src/fastmcp/server/server.py CHANGED
@@ -42,6 +42,7 @@ from starlette.routing import BaseRoute, Route
42
 
43
  import fastmcp
44
  import fastmcp.server
 
45
  from fastmcp.exceptions import DisabledError, NotFoundError
46
  from fastmcp.prompts import Prompt, PromptManager
47
  from fastmcp.prompts.prompt import FunctionPrompt
@@ -54,7 +55,7 @@ from fastmcp.server.http import (
54
  create_sse_app,
55
  create_streamable_http_app,
56
  )
57
- from fastmcp.server.middleware.middleware import MCPMiddleware, MiddlewareContext
58
  from fastmcp.settings import Settings
59
  from fastmcp.tools import ToolManager
60
  from fastmcp.tools.tool import FunctionTool, Tool
@@ -340,28 +341,7 @@ class FastMCP(Generic[LifespanResultT]):
340
 
341
  async def get_tools(self) -> dict[str, Tool]:
342
  """Get all registered tools, indexed by registered key."""
343
- if (tools := self._cache.get("tools")) is self._cache.NOT_FOUND:
344
- tools: dict[str, Tool] = {}
345
-
346
- # iterate such that new mounts overwrite older ones
347
- for mounted_server in self._mounted_servers:
348
- try:
349
- server_tools = await mounted_server.server.get_tools()
350
- # Apply prefix to each tool key if prefix exists and is not empty
351
- if mounted_server.prefix:
352
- for tool in server_tools.values():
353
- tool = tool.with_key(f"{mounted_server.prefix}_{tool.key}")
354
- tools[tool.key] = tool
355
- else:
356
- tools.update(server_tools)
357
- except Exception as e:
358
- logger.warning(
359
- f"Failed to get tools from mounted server '{mounted_server.prefix}': {e}"
360
- )
361
- continue
362
- tools.update(self._tool_manager.get_tools())
363
- self._cache.set("tools", tools)
364
- return tools
365
 
366
  async def get_tool(self, key: str) -> Tool:
367
  tools = await self.get_tools()
@@ -529,18 +509,23 @@ class FastMCP(Generic[LifespanResultT]):
529
  return decorator
530
 
531
  async def _mcp_list_tools(self) -> list[MCPTool]:
 
 
 
 
 
 
 
532
  """
533
  List all available tools, in the format expected by the low-level MCP
534
  server.
535
 
536
  """
537
- logger.debug("List tools")
538
 
539
- async def _final_handler(
540
- context: MiddlewareContext[dict[str, Any]],
541
  ) -> list[MCPTool]:
542
- # Call the business logic method
543
- tools = await self.get_tools()
544
 
545
  mcp_tools: list[MCPTool] = []
546
  for key, tool in tools.items():
@@ -552,7 +537,7 @@ class FastMCP(Generic[LifespanResultT]):
552
  with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
553
  # Create the middleware context.
554
  mw_context = MiddlewareContext(
555
- message={}, # List tools doesn't have parameters
556
  source="client",
557
  type="request",
558
  method="tools/list",
@@ -560,7 +545,41 @@ class FastMCP(Generic[LifespanResultT]):
560
  )
561
 
562
  # Apply the middleware chain.
563
- return await self._apply_middleware(mw_context, _final_handler)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
564
 
565
  async def _mcp_list_resources(self) -> list[MCPResource]:
566
  """
@@ -568,12 +587,11 @@ class FastMCP(Generic[LifespanResultT]):
568
  server.
569
 
570
  """
571
- logger.debug("List resources")
572
 
573
  async def _final_handler(
574
  context: MiddlewareContext[dict[str, Any]],
575
  ) -> list[MCPResource]:
576
- # Call the business logic method
577
  resources = await self.get_resources()
578
  mcp_resources: list[MCPResource] = []
579
  for key, resource in resources.items():
@@ -600,12 +618,11 @@ class FastMCP(Generic[LifespanResultT]):
600
  MCP server.
601
 
602
  """
603
- logger.debug("List resource templates")
604
 
605
  async def _final_handler(
606
  context: MiddlewareContext[dict[str, Any]],
607
  ) -> list[MCPResourceTemplate]:
608
- # Call the business logic method
609
  templates = await self.get_resource_templates()
610
  mcp_templates: list[MCPResourceTemplate] = []
611
  for key, template in templates.items():
@@ -632,12 +649,11 @@ class FastMCP(Generic[LifespanResultT]):
632
  server.
633
 
634
  """
635
- logger.debug("List prompts")
636
 
637
  async def _final_handler(
638
  context: MiddlewareContext[dict[str, Any]],
639
  ) -> list[MCPPrompt]:
640
- # Call the business logic method
641
  prompts = await self.get_prompts()
642
  mcp_prompts: list[MCPPrompt] = []
643
  for key, prompt in prompts.items():
@@ -673,7 +689,7 @@ class FastMCP(Generic[LifespanResultT]):
673
  Returns:
674
  List of MCP Content objects containing the tool results
675
  """
676
- logger.debug("Call tool: %s with %s", key, arguments)
677
 
678
  with fastmcp.server.context.Context(fastmcp=self):
679
  try:
@@ -693,18 +709,15 @@ class FastMCP(Generic[LifespanResultT]):
693
  """
694
 
695
  async def _handler(
696
- context: MiddlewareContext[mcp.types.CallToolRequest],
697
  ) -> list[MCPContent]:
698
  return await self._call_tool(
699
- key=context.message.params.name,
700
- arguments=context.message.params.arguments or {},
701
  )
702
 
703
  mw_context = MiddlewareContext(
704
- message=mcp.types.CallToolRequest(
705
- method="tools/call",
706
- params=mcp.types.CallToolRequestParams(name=key, arguments=arguments),
707
- ),
708
  source="client",
709
  type="request",
710
  method="tools/call",
@@ -758,7 +771,7 @@ class FastMCP(Generic[LifespanResultT]):
758
 
759
  Delegates to _read_resource, which should be overridden by FastMCP subclasses.
760
  """
761
- logger.debug("Read resource: %s", uri)
762
 
763
  with fastmcp.server.context.Context(fastmcp=self):
764
  try:
@@ -779,10 +792,10 @@ class FastMCP(Generic[LifespanResultT]):
779
  """
780
 
781
  async def _handler(
782
- context: MiddlewareContext[mcp.types.ReadResourceRequest],
783
  ) -> list[ReadResourceContents]:
784
  return await self._read_resource(
785
- uri=context.message.params.uri,
786
  )
787
 
788
  # Convert string URI to AnyUrl if needed
@@ -794,10 +807,7 @@ class FastMCP(Generic[LifespanResultT]):
794
  uri_param = uri
795
 
796
  mw_context = MiddlewareContext(
797
- message=mcp.types.ReadResourceRequest(
798
- method="resources/read",
799
- params=mcp.types.ReadResourceRequestParams(uri=uri_param),
800
- ),
801
  source="client",
802
  type="request",
803
  method="resources/read",
@@ -857,7 +867,7 @@ class FastMCP(Generic[LifespanResultT]):
857
 
858
  Delegates to _get_prompt, which should be overridden by FastMCP subclasses.
859
  """
860
- logger.debug("Get prompt: %s with %s", name, arguments)
861
 
862
  with fastmcp.server.context.Context(fastmcp=self):
863
  try:
@@ -879,18 +889,15 @@ class FastMCP(Generic[LifespanResultT]):
879
  """
880
 
881
  async def _handler(
882
- context: MiddlewareContext[mcp.types.GetPromptRequest],
883
  ) -> GetPromptResult:
884
  return await self._get_prompt(
885
- name=context.message.params.name,
886
- arguments=context.message.params.arguments,
887
  )
888
 
889
  mw_context = MiddlewareContext(
890
- message=mcp.types.GetPromptRequest(
891
- method="prompts/get",
892
- params=mcp.types.GetPromptRequestParams(name=name, arguments=arguments),
893
- ),
894
  source="client",
895
  type="request",
896
  method="prompts/get",
 
42
 
43
  import fastmcp
44
  import fastmcp.server
45
+ import fastmcp.server.middleware
46
  from fastmcp.exceptions import DisabledError, NotFoundError
47
  from fastmcp.prompts import Prompt, PromptManager
48
  from fastmcp.prompts.prompt import FunctionPrompt
 
55
  create_sse_app,
56
  create_streamable_http_app,
57
  )
58
+ from fastmcp.server.middleware import MCPMiddleware, MiddlewareContext
59
  from fastmcp.settings import Settings
60
  from fastmcp.tools import ToolManager
61
  from fastmcp.tools.tool import FunctionTool, Tool
 
341
 
342
  async def get_tools(self) -> dict[str, Tool]:
343
  """Get all registered tools, indexed by registered key."""
344
+ return await self._list_tools(apply_middleware=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
 
346
  async def get_tool(self, key: str) -> Tool:
347
  tools = await self.get_tools()
 
509
  return decorator
510
 
511
  async def _mcp_list_tools(self) -> list[MCPTool]:
512
+ logger.debug("Handler called: list_tools")
513
+
514
+ with fastmcp.server.context.Context(fastmcp=self):
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) -> dict[str, Tool]:
519
  """
520
  List all available tools, in the format expected by the low-level MCP
521
  server.
522
 
523
  """
 
524
 
525
+ async def _handler(
526
+ context: MiddlewareContext[mcp.types.ListToolsRequest],
527
  ) -> list[MCPTool]:
528
+ tools = await self._list_tools()
 
529
 
530
  mcp_tools: list[MCPTool] = []
531
  for key, tool in tools.items():
 
537
  with fastmcp.server.context.Context(fastmcp=self) as fastmcp_ctx:
538
  # Create the middleware context.
539
  mw_context = MiddlewareContext(
540
+ message=mcp.types.ListToolsRequest(method="tools/list"),
541
  source="client",
542
  type="request",
543
  method="tools/list",
 
545
  )
546
 
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) -> dict[str, Tool]:
551
+ """
552
+ List all available tools.
553
+ """
554
+
555
+ if (tools := self._cache.get("tools")) is self._cache.NOT_FOUND:
556
+ tools: dict[str, Tool] = {}
557
+
558
+ # iterate such that new mounts overwrite older ones
559
+ for mounted_server in self._mounted_servers:
560
+ try:
561
+ if apply_middleware:
562
+ server_tools = (
563
+ await mounted_server.server._middleware_list_tools()
564
+ )
565
+ else:
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.values():
570
+ tool = tool.with_key(f"{mounted_server.prefix}_{tool.key}")
571
+ tools[tool.key] = tool
572
+ else:
573
+ tools.update(server_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.update(self._tool_manager.get_tools())
581
+ self._cache.set("tools", tools)
582
+ return tools
583
 
584
  async def _mcp_list_resources(self) -> list[MCPResource]:
585
  """
 
587
  server.
588
 
589
  """
590
+ logger.debug("Handler called: list_resources")
591
 
592
  async def _final_handler(
593
  context: MiddlewareContext[dict[str, Any]],
594
  ) -> list[MCPResource]:
 
595
  resources = await self.get_resources()
596
  mcp_resources: list[MCPResource] = []
597
  for key, resource in resources.items():
 
618
  MCP server.
619
 
620
  """
621
+ logger.debug("Handler called: list_resource_templates")
622
 
623
  async def _final_handler(
624
  context: MiddlewareContext[dict[str, Any]],
625
  ) -> list[MCPResourceTemplate]:
 
626
  templates = await self.get_resource_templates()
627
  mcp_templates: list[MCPResourceTemplate] = []
628
  for key, template in templates.items():
 
649
  server.
650
 
651
  """
652
+ logger.debug("Handler called: list_prompts")
653
 
654
  async def _final_handler(
655
  context: MiddlewareContext[dict[str, Any]],
656
  ) -> list[MCPPrompt]:
 
657
  prompts = await self.get_prompts()
658
  mcp_prompts: list[MCPPrompt] = []
659
  for key, prompt in prompts.items():
 
689
  Returns:
690
  List of MCP Content objects containing the tool results
691
  """
692
+ logger.debug("Handler called: call_tool %s with %s", key, arguments)
693
 
694
  with fastmcp.server.context.Context(fastmcp=self):
695
  try:
 
709
  """
710
 
711
  async def _handler(
712
+ context: MiddlewareContext[mcp.types.CallToolRequestParams],
713
  ) -> list[MCPContent]:
714
  return await self._call_tool(
715
+ key=context.message.name,
716
+ arguments=context.message.arguments or {},
717
  )
718
 
719
  mw_context = MiddlewareContext(
720
+ message=mcp.types.CallToolRequestParams(name=key, arguments=arguments),
 
 
 
721
  source="client",
722
  type="request",
723
  method="tools/call",
 
771
 
772
  Delegates to _read_resource, which should be overridden by FastMCP subclasses.
773
  """
774
+ logger.debug("Handler called: read_resource %s", uri)
775
 
776
  with fastmcp.server.context.Context(fastmcp=self):
777
  try:
 
792
  """
793
 
794
  async def _handler(
795
+ context: MiddlewareContext[mcp.types.ReadResourceRequestParams],
796
  ) -> list[ReadResourceContents]:
797
  return await self._read_resource(
798
+ uri=context.message.uri,
799
  )
800
 
801
  # Convert string URI to AnyUrl if needed
 
807
  uri_param = uri
808
 
809
  mw_context = MiddlewareContext(
810
+ message=mcp.types.ReadResourceRequestParams(uri=uri_param),
 
 
 
811
  source="client",
812
  type="request",
813
  method="resources/read",
 
867
 
868
  Delegates to _get_prompt, which should be overridden by FastMCP subclasses.
869
  """
870
+ logger.debug("Handler called: get_prompt %s with %s", name, arguments)
871
 
872
  with fastmcp.server.context.Context(fastmcp=self):
873
  try:
 
889
  """
890
 
891
  async def _handler(
892
+ context: MiddlewareContext[mcp.types.GetPromptRequestParams],
893
  ) -> GetPromptResult:
894
  return await self._get_prompt(
895
+ name=context.message.name,
896
+ arguments=context.message.arguments,
897
  )
898
 
899
  mw_context = MiddlewareContext(
900
+ message=mcp.types.GetPromptRequestParams(name=name, arguments=arguments),
 
 
 
901
  source="client",
902
  type="request",
903
  method="prompts/get",