fastmcp 2.7.0__py3-none-any.whl → 2.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -67,6 +67,7 @@ def get_http_headers(include_all: bool = False) -> dict[str, str]:
67
67
  "te",
68
68
  "keep-alive",
69
69
  "expect",
70
+ "accept",
70
71
  # Proxy-related headers
71
72
  "proxy-authenticate",
72
73
  "proxy-authorization",
fastmcp/server/http.py CHANGED
@@ -13,6 +13,7 @@ from mcp.server.auth.middleware.bearer_auth import (
13
13
  from mcp.server.auth.routes import create_auth_routes
14
14
  from mcp.server.lowlevel.server import LifespanResultT
15
15
  from mcp.server.sse import SseServerTransport
16
+ from mcp.server.streamable_http import EventStore
16
17
  from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
17
18
  from starlette.applications import Starlette
18
19
  from starlette.middleware import Middleware
@@ -241,7 +242,7 @@ def create_sse_app(
241
242
  def create_streamable_http_app(
242
243
  server: FastMCP[LifespanResultT],
243
244
  streamable_http_path: str,
244
- event_store: None = None,
245
+ event_store: EventStore | None = None,
245
246
  auth: OAuthProvider | None = None,
246
247
  json_response: bool = False,
247
248
  stateless_http: bool = False,
fastmcp/server/openapi.py CHANGED
@@ -103,15 +103,27 @@ class RouteType(enum.Enum):
103
103
  IGNORE = "IGNORE"
104
104
 
105
105
 
106
- @dataclass
106
+ @dataclass(kw_only=True)
107
107
  class RouteMap:
108
108
  """Mapping configuration for HTTP routes to FastMCP component types."""
109
109
 
110
110
  methods: list[HttpMethod] | Literal["*"] = field(default="*")
111
111
  pattern: Pattern[str] | str = field(default=r".*")
112
- mcp_type: MCPType | None = field(default=None)
113
112
  route_type: RouteType | MCPType | None = field(default=None)
114
- tags: set[str] = field(default_factory=set)
113
+ tags: set[str] = field(
114
+ default_factory=set,
115
+ metadata={"description": "A set of tags to match. All tags must match."},
116
+ )
117
+ mcp_type: MCPType | None = field(
118
+ default=None,
119
+ metadata={"description": "The type of FastMCP component to create."},
120
+ )
121
+ mcp_tags: set[str] = field(
122
+ default_factory=set,
123
+ metadata={
124
+ "description": "A set of tags to apply to the generated FastMCP component."
125
+ },
126
+ )
115
127
 
116
128
  def __post_init__(self):
117
129
  """Validate and process the route map after initialization."""
@@ -155,23 +167,17 @@ class RouteMap:
155
167
  self.route_type = self.mcp_type
156
168
 
157
169
 
158
- # Default route mappings as a list, where order determines priority
170
+ # Default route mapping: all routes become tools.
171
+ # Users can provide custom route_maps to override this behavior.
159
172
  DEFAULT_ROUTE_MAPPINGS = [
160
- # GET requests with path parameters go to ResourceTemplate
161
- RouteMap(
162
- methods=["GET"], pattern=r".*\{.*\}.*", mcp_type=MCPType.RESOURCE_TEMPLATE
163
- ),
164
- # GET requests without path parameters go to Resource
165
- RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE),
166
- # All other HTTP methods go to Tool
167
- RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL),
173
+ RouteMap(mcp_type=MCPType.TOOL),
168
174
  ]
169
175
 
170
176
 
171
177
  def _determine_route_type(
172
178
  route: openapi.HTTPRoute,
173
179
  mappings: list[RouteMap],
174
- ) -> MCPType:
180
+ ) -> RouteMap:
175
181
  """
176
182
  Determines the FastMCP component type based on the route and mappings.
177
183
 
@@ -180,7 +186,7 @@ def _determine_route_type(
180
186
  mappings: List of RouteMap objects in priority order
181
187
 
182
188
  Returns:
183
- MCPType for this route
189
+ The RouteMap that matches the route, or a catchall "Tool" RouteMap if no match is found.
184
190
  """
185
191
  # Check mappings in priority order (first match wins)
186
192
  for route_map in mappings:
@@ -207,10 +213,10 @@ def _determine_route_type(
207
213
  logger.debug(
208
214
  f"Route {route.method} {route.path} matched mapping to {route_map.mcp_type.name}"
209
215
  )
210
- return route_map.mcp_type
216
+ return route_map
211
217
 
212
218
  # Default fallback
213
- return MCPType.TOOL
219
+ return RouteMap(mcp_type=MCPType.TOOL)
214
220
 
215
221
 
216
222
  class OpenAPITool(Tool):
@@ -223,19 +229,17 @@ class OpenAPITool(Tool):
223
229
  name: str,
224
230
  description: str,
225
231
  parameters: dict[str, Any],
226
- tags: set[str] = set(),
232
+ tags: set[str] | None = None,
227
233
  timeout: float | None = None,
228
234
  annotations: ToolAnnotations | None = None,
229
- exclude_args: list[str] | None = None,
230
235
  serializer: Callable[[Any], str] | None = None,
231
236
  ):
232
237
  super().__init__(
233
238
  name=name,
234
239
  description=description,
235
240
  parameters=parameters,
236
- tags=tags,
241
+ tags=tags or set(),
237
242
  annotations=annotations,
238
- exclude_args=exclude_args,
239
243
  serializer=serializer,
240
244
  )
241
245
  self._client = client
@@ -688,6 +692,7 @@ class FastMCPOpenAPI(FastMCP):
688
692
  route_map_fn: RouteMapFn | None = None,
689
693
  mcp_component_fn: ComponentFn | None = None,
690
694
  mcp_names: dict[str, str] | None = None,
695
+ tags: set[str] | None = None,
691
696
  timeout: float | None = None,
692
697
  **settings: Any,
693
698
  ):
@@ -710,6 +715,8 @@ class FastMCPOpenAPI(FastMCP):
710
715
  operationId up to the first double underscore. If no operationId exists,
711
716
  falls back to slugified summary or path-based naming.
712
717
  All names are truncated to 56 characters maximum.
718
+ tags: Optional set of tags to add to all components. Components always receive any tags
719
+ from the route.
713
720
  timeout: Optional timeout (in seconds) for all requests
714
721
  **settings: Additional settings for FastMCP
715
722
  """
@@ -717,9 +724,7 @@ class FastMCPOpenAPI(FastMCP):
717
724
 
718
725
  self._client = client
719
726
  self._timeout = timeout
720
- self._route_map_fn = route_map_fn
721
727
  self._mcp_component_fn = mcp_component_fn
722
- self._mcp_names = mcp_names or {}
723
728
 
724
729
  # Keep track of names to detect collisions
725
730
  self._used_names = {
@@ -735,12 +740,16 @@ class FastMCPOpenAPI(FastMCP):
735
740
  route_maps = (route_maps or []) + DEFAULT_ROUTE_MAPPINGS
736
741
  for route in http_routes:
737
742
  # Determine route type based on mappings or default rules
738
- route_type = _determine_route_type(route, route_maps)
743
+ route_map = _determine_route_type(route, route_maps)
744
+
745
+ # TODO: remove this once RouteType is removed and mcp_type is typed as MCPType without | None
746
+ assert route_map.mcp_type is not None
747
+ route_type = route_map.mcp_type
739
748
 
740
749
  # Call route_map_fn if provided
741
- if self._route_map_fn is not None:
750
+ if route_map_fn is not None:
742
751
  try:
743
- result = self._route_map_fn(route, route_type)
752
+ result = route_map_fn(route, route_type)
744
753
  if result is not None:
745
754
  route_type = result
746
755
  logger.debug(
@@ -754,29 +763,32 @@ class FastMCPOpenAPI(FastMCP):
754
763
  )
755
764
 
756
765
  # Generate a default name from the route
757
- component_name = self._generate_default_name(route, route_type)
766
+ component_name = self._generate_default_name(route, mcp_names)
767
+
768
+ route_tags = set(route.tags) | route_map.mcp_tags | (tags or set())
758
769
 
759
770
  if route_type == MCPType.TOOL:
760
- self._create_openapi_tool(route, component_name)
771
+ self._create_openapi_tool(route, component_name, tags=route_tags)
761
772
  elif route_type == MCPType.RESOURCE:
762
- self._create_openapi_resource(route, component_name)
773
+ self._create_openapi_resource(route, component_name, tags=route_tags)
763
774
  elif route_type == MCPType.RESOURCE_TEMPLATE:
764
- self._create_openapi_template(route, component_name)
775
+ self._create_openapi_template(route, component_name, tags=route_tags)
765
776
  elif route_type == MCPType.EXCLUDE:
766
777
  logger.info(f"Excluding route: {route.method} {route.path}")
767
778
 
768
779
  logger.info(f"Created FastMCP OpenAPI server with {len(http_routes)} routes")
769
780
 
770
781
  def _generate_default_name(
771
- self, route: openapi.HTTPRoute, mcp_type: MCPType
782
+ self, route: openapi.HTTPRoute, mcp_names_map: dict[str, str] | None = None
772
783
  ) -> str:
773
784
  """Generate a default name from the route using the configured strategy."""
774
785
  name = ""
786
+ mcp_names_map = mcp_names_map or {}
775
787
 
776
788
  # First check if there's a custom mapping for this operationId
777
789
  if route.operation_id:
778
- if route.operation_id in self._mcp_names:
779
- name = self._mcp_names[route.operation_id]
790
+ if route.operation_id in mcp_names_map:
791
+ name = mcp_names_map[route.operation_id]
780
792
  else:
781
793
  # If there's a double underscore in the operationId, use the first part
782
794
  name = route.operation_id.split("__")[0]
@@ -821,7 +833,12 @@ class FastMCPOpenAPI(FastMCP):
821
833
 
822
834
  return new_name
823
835
 
824
- def _create_openapi_tool(self, route: openapi.HTTPRoute, name: str):
836
+ def _create_openapi_tool(
837
+ self,
838
+ route: openapi.HTTPRoute,
839
+ name: str,
840
+ tags: set[str],
841
+ ):
825
842
  """Creates and registers an OpenAPITool with enhanced description."""
826
843
  combined_schema = _combine_schemas(route)
827
844
 
@@ -848,7 +865,7 @@ class FastMCPOpenAPI(FastMCP):
848
865
  name=tool_name,
849
866
  description=enhanced_description,
850
867
  parameters=combined_schema,
851
- tags=set(route.tags or []),
868
+ tags=set(route.tags or []) | tags,
852
869
  timeout=self._timeout,
853
870
  )
854
871
 
@@ -869,7 +886,12 @@ class FastMCPOpenAPI(FastMCP):
869
886
  f"Registered TOOL: {tool_name} ({route.method} {route.path}) with tags: {route.tags}"
870
887
  )
871
888
 
872
- def _create_openapi_resource(self, route: openapi.HTTPRoute, name: str):
889
+ def _create_openapi_resource(
890
+ self,
891
+ route: openapi.HTTPRoute,
892
+ name: str,
893
+ tags: set[str],
894
+ ):
873
895
  """Creates and registers an OpenAPIResource with enhanced description."""
874
896
  # Get a unique resource name
875
897
  resource_name = self._get_unique_name(name, "resource")
@@ -893,7 +915,7 @@ class FastMCPOpenAPI(FastMCP):
893
915
  uri=resource_uri,
894
916
  name=resource_name,
895
917
  description=enhanced_description,
896
- tags=set(route.tags or []),
918
+ tags=set(route.tags or []) | tags,
897
919
  timeout=self._timeout,
898
920
  )
899
921
 
@@ -914,7 +936,12 @@ class FastMCPOpenAPI(FastMCP):
914
936
  f"Registered RESOURCE: {resource_uri} ({route.method} {route.path}) with tags: {route.tags}"
915
937
  )
916
938
 
917
- def _create_openapi_template(self, route: openapi.HTTPRoute, name: str):
939
+ def _create_openapi_template(
940
+ self,
941
+ route: openapi.HTTPRoute,
942
+ name: str,
943
+ tags: set[str],
944
+ ):
918
945
  """Creates and registers an OpenAPIResourceTemplate with enhanced description."""
919
946
  # Get a unique template name
920
947
  template_name = self._get_unique_name(name, "resource_template")
@@ -967,7 +994,7 @@ class FastMCPOpenAPI(FastMCP):
967
994
  name=template_name,
968
995
  description=enhanced_description,
969
996
  parameters=template_params_schema,
970
- tags=set(route.tags or []),
997
+ tags=set(route.tags or []) | tags,
971
998
  timeout=self._timeout,
972
999
  )
973
1000
 
fastmcp/server/proxy.py CHANGED
@@ -186,8 +186,10 @@ class FastMCPProxy(FastMCP):
186
186
  else:
187
187
  raise e
188
188
  for tool in client_tools:
189
- tool_proxy = await ProxyTool.from_client(self.client, tool)
190
- tools[tool_proxy.name] = tool_proxy
189
+ # don't overwrite tools defined in the server
190
+ if tool.name not in tools:
191
+ tool_proxy = await ProxyTool.from_client(self.client, tool)
192
+ tools[tool_proxy.name] = tool_proxy
191
193
 
192
194
  return tools
193
195
 
@@ -203,8 +205,12 @@ class FastMCPProxy(FastMCP):
203
205
  else:
204
206
  raise e
205
207
  for resource in client_resources:
206
- resource_proxy = await ProxyResource.from_client(self.client, resource)
207
- resources[str(resource_proxy.uri)] = resource_proxy
208
+ # don't overwrite resources defined in the server
209
+ if str(resource.uri) not in resources:
210
+ resource_proxy = await ProxyResource.from_client(
211
+ self.client, resource
212
+ )
213
+ resources[str(resource_proxy.uri)] = resource_proxy
208
214
 
209
215
  return resources
210
216
 
@@ -220,8 +226,12 @@ class FastMCPProxy(FastMCP):
220
226
  else:
221
227
  raise e
222
228
  for template in client_templates:
223
- template_proxy = await ProxyTemplate.from_client(self.client, template)
224
- templates[template_proxy.uri_template] = template_proxy
229
+ # don't overwrite templates defined in the server
230
+ if template.uriTemplate not in templates:
231
+ template_proxy = await ProxyTemplate.from_client(
232
+ self.client, template
233
+ )
234
+ templates[template_proxy.uri_template] = template_proxy
225
235
 
226
236
  return templates
227
237
 
@@ -237,24 +247,27 @@ class FastMCPProxy(FastMCP):
237
247
  else:
238
248
  raise e
239
249
  for prompt in client_prompts:
240
- prompt_proxy = await ProxyPrompt.from_client(self.client, prompt)
241
- prompts[prompt_proxy.name] = prompt_proxy
250
+ # don't overwrite prompts defined in the server
251
+ if prompt.name not in prompts:
252
+ prompt_proxy = await ProxyPrompt.from_client(self.client, prompt)
253
+ prompts[prompt_proxy.name] = prompt_proxy
254
+
242
255
  return prompts
243
256
 
244
- async def _mcp_call_tool(
257
+ async def _call_tool(
245
258
  self, key: str, arguments: dict[str, Any]
246
259
  ) -> list[TextContent | ImageContent | EmbeddedResource]:
247
260
  try:
248
- result = await super()._mcp_call_tool(key, arguments)
261
+ result = await super()._call_tool(key, arguments)
249
262
  return result
250
263
  except NotFoundError:
251
264
  async with self.client:
252
265
  result = await self.client.call_tool(key, arguments)
253
266
  return result
254
267
 
255
- async def _mcp_read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
268
+ async def _read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
256
269
  try:
257
- result = await super()._mcp_read_resource(uri)
270
+ result = await super()._read_resource(uri)
258
271
  return result
259
272
  except NotFoundError:
260
273
  async with self.client:
@@ -270,11 +283,11 @@ class FastMCPProxy(FastMCP):
270
283
  ReadResourceContents(content=content, mime_type=resource[0].mimeType)
271
284
  ]
272
285
 
273
- async def _mcp_get_prompt(
286
+ async def _get_prompt(
274
287
  self, name: str, arguments: dict[str, Any] | None = None
275
288
  ) -> GetPromptResult:
276
289
  try:
277
- result = await super()._mcp_get_prompt(name, arguments)
290
+ result = await super()._get_prompt(name, arguments)
278
291
  return result
279
292
  except NotFoundError:
280
293
  async with self.client: