Jeremiah Lowin commited on
Commit
f477a70
·
1 Parent(s): 4a59792

Update route map logic

Browse files
docs/patterns/openapi.mdx CHANGED
@@ -64,37 +64,34 @@ DEFAULT_ROUTE_MAPPINGS = [
64
  RouteMap(
65
  methods=["GET"],
66
  pattern=r".*\{.*\}.*",
67
- route_type=RouteType.RESOURCE_TEMPLATE,
68
  ),
69
 
70
  # GET without path parameters -> Resource
71
  RouteMap(
72
  methods=["GET"],
73
  pattern=r".*",
74
- route_type=RouteType.RESOURCE,
75
  ),
76
 
77
  # All other methods -> Tool
78
- RouteMap(
79
- methods="*",
80
- pattern=r".*",
81
- route_type=RouteType.TOOL,
82
- ),
83
  ]
84
  ```
 
85
  ### Custom Route Maps
86
 
87
  Users can add custom route maps to override the default mapping behavior. User-supplied route maps are always applied first, before the default route maps.
88
 
89
  ```python
90
- from fastmcp.server.openapi import RouteMap, RouteType
91
 
92
  # Custom mapping rules
93
  custom_maps = [
94
  # Force all analytics endpoints to be Tools
95
  RouteMap(methods=["GET"],
96
  pattern=r"^/analytics/.*",
97
- route_type=RouteType.TOOL)
98
  ]
99
 
100
  # Apply custom mappings
@@ -105,6 +102,9 @@ mcp = await FastMCP.from_openapi(
105
  )
106
  ```
107
 
 
 
 
108
 
109
  ### All Routes as Tools
110
 
@@ -127,13 +127,46 @@ mcp = FastMCP.from_openapi(
127
  openapi_spec=spec,
128
  client=api_client,
129
  route_maps=[
130
- RouteMap(methods="*", pattern=r".*", route_type=RouteType.TOOL)
131
  ]
132
  )
133
  ```
134
 
135
  Note that `all_routes_as_tools` and `route_maps` cannot be used together - if you need more complex mapping rules, use `route_maps` instead.
136
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  ## How It Works
138
 
139
  1. FastMCP parses your OpenAPI spec to extract routes and schemas
@@ -261,3 +294,68 @@ if __name__ == "__main__":
261
  mcp.run()
262
  ```
263
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  RouteMap(
65
  methods=["GET"],
66
  pattern=r".*\{.*\}.*",
67
+ mcp_type=MCPType.RESOURCE_TEMPLATE,
68
  ),
69
 
70
  # GET without path parameters -> Resource
71
  RouteMap(
72
  methods=["GET"],
73
  pattern=r".*",
74
+ mcp_type=MCPType.RESOURCE,
75
  ),
76
 
77
  # All other methods -> Tool
78
+ ALL_TOOLS(),
 
 
 
 
79
  ]
80
  ```
81
+
82
  ### Custom Route Maps
83
 
84
  Users can add custom route maps to override the default mapping behavior. User-supplied route maps are always applied first, before the default route maps.
85
 
86
  ```python
87
+ from fastmcp.server.openapi import RouteMap, MCPType
88
 
89
  # Custom mapping rules
90
  custom_maps = [
91
  # Force all analytics endpoints to be Tools
92
  RouteMap(methods=["GET"],
93
  pattern=r"^/analytics/.*",
94
+ mcp_type=MCPType.TOOL)
95
  ]
96
 
97
  # Apply custom mappings
 
102
  )
103
  ```
104
 
105
+ <Info>
106
+ For backward compatibility, FastMCP still supports the `route_type` parameter and `RouteType` enum, but they are deprecated and will be removed in a future version. You will see deprecation warnings if you use them.
107
+ </Info>
108
 
109
  ### All Routes as Tools
110
 
 
127
  openapi_spec=spec,
128
  client=api_client,
129
  route_maps=[
130
+ RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)
131
  ]
132
  )
133
  ```
134
 
135
  Note that `all_routes_as_tools` and `route_maps` cannot be used together - if you need more complex mapping rules, use `route_maps` instead.
136
 
137
+ ### Excluding Routes
138
+
139
+ If you want to exclude certain routes from being converted to MCP components, you can map them to `MCPType.EXCLUDE`. This is useful for endpoints that should not be accessible to the agent.
140
+
141
+ ```python
142
+ from fastmcp.server.openapi import RouteMap, MCPType
143
+
144
+ # Custom mapping rules to exclude specific routes
145
+ custom_maps = [
146
+ # Exclude all admin endpoints
147
+ RouteMap(
148
+ methods="*",
149
+ pattern=r"^/admin/.*",
150
+ mcp_type=MCPType.EXCLUDE
151
+ ),
152
+ # Exclude analytics GET endpoints
153
+ RouteMap(
154
+ methods=["GET"],
155
+ pattern=r"^/analytics/.*",
156
+ mcp_type=MCPType.EXCLUDE
157
+ )
158
+ ]
159
+
160
+ # Apply custom mappings
161
+ mcp = FastMCP.from_openapi(
162
+ openapi_spec=spec,
163
+ client=api_client,
164
+ route_maps=custom_maps
165
+ )
166
+ ```
167
+
168
+ When a route is mapped to `MCPType.EXCLUDE`, FastMCP will log its presence but won't create any MCP component for it, effectively making it invisible to clients and agents using the MCP server.
169
+
170
  ## How It Works
171
 
172
  1. FastMCP parses your OpenAPI spec to extract routes and schemas
 
294
  mcp.run()
295
  ```
296
 
297
+ ### Route Map Shortcuts
298
+
299
+ FastMCP provides several shortcut functions to create common route maps more easily:
300
+
301
+ ```python
302
+ from fastmcp.server.openapi import (
303
+ ALL_TOOLS,
304
+ EXCLUDE_ALL,
305
+ EXCLUDE_PATTERN,
306
+ PATTERN_AS_TOOLS,
307
+ )
308
+
309
+ # Create an MCP server with custom route maps using shortcuts
310
+ mcp = FastMCP.from_openapi(
311
+ openapi_spec=spec,
312
+ client=api_client,
313
+ route_maps=[
314
+ # First exclude all admin endpoints
315
+ EXCLUDE_PATTERN(r"^/admin/.*"),
316
+
317
+ # Make all /api/v1 endpoints tools
318
+ PATTERN_AS_TOOLS(r"^/api/v1/.*"),
319
+
320
+ # Make all remaining routes tools
321
+ ALL_TOOLS(),
322
+ ]
323
+ )
324
+ ```
325
+
326
+ Available shortcuts:
327
+
328
+ | Shortcut Function | Description |
329
+ |------------------|-------------|
330
+ | `ALL_TOOLS()` | Converts all matching routes to tools |
331
+ | `EXCLUDE_ALL()` | Excludes all matching routes from being converted to any component |
332
+ | `PATTERN_AS_TOOLS(pattern)` | Converts routes matching a specific pattern to tools |
333
+ | `EXCLUDE_PATTERN(pattern)` | Excludes routes matching a specific pattern |
334
+
335
+ These shortcuts are particularly useful for:
336
+
337
+ 1. Converting all remaining unmatched routes to tools (use `ALL_TOOLS()`)
338
+ 2. Excluding whole sections of your API (use `EXCLUDE_PATTERN("/path/.*")`)
339
+ 3. Converting routes matching specific patterns to tools (use `PATTERN_AS_TOOLS("/path/.*")`)
340
+
341
+ The `all_routes_as_tools=True` parameter is equivalent to using just `[ALL_TOOLS()]` as your route maps.
342
+
343
+ <Tip>
344
+ You can use `EXCLUDE_ALL()` as the last entry in your custom route maps to completely ignore the default route maps. Since custom route maps are applied first and default maps are appended afterward, having `EXCLUDE_ALL()` at the end of your custom maps will match any routes that your earlier custom rules didn't match, preventing the default maps from having any effect.
345
+
346
+ ```python
347
+ # Create server that only uses custom route maps, ignoring defaults
348
+ mcp = FastMCP.from_openapi(
349
+ openapi_spec=spec,
350
+ client=api_client,
351
+ route_maps=[
352
+ # Routes to keep as tools
353
+ PATTERN_AS_TOOLS(r"^/api/v1/.*"),
354
+
355
+ # Exclude everything else (ignores default route maps)
356
+ EXCLUDE_ALL(),
357
+ ]
358
+ )
359
+ ```
360
+ </Tip>
361
+
src/fastmcp/server/openapi.py CHANGED
@@ -5,8 +5,9 @@ from __future__ import annotations
5
  import enum
6
  import json
7
  import re
 
8
  from collections.abc import Callable
9
- from dataclasses import dataclass
10
  from re import Pattern
11
  from typing import TYPE_CHECKING, Any, Literal
12
 
@@ -33,46 +34,176 @@ logger = get_logger(__name__)
33
  HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]
34
 
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  class RouteType(enum.Enum):
37
- """Type of FastMCP component to create from a route."""
 
 
 
 
38
 
39
  TOOL = "TOOL"
40
  RESOURCE = "RESOURCE"
41
  RESOURCE_TEMPLATE = "RESOURCE_TEMPLATE"
42
  PROMPT = "PROMPT"
43
- IGNORE = "IGNORE"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
 
46
  @dataclass
47
  class RouteMap:
48
  """Mapping configuration for HTTP routes to FastMCP component types."""
49
 
50
- methods: list[HttpMethod] | Literal["*"]
51
- pattern: Pattern[str] | str
52
- route_type: RouteType
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
 
55
  # Default route mappings as a list, where order determines priority
56
  DEFAULT_ROUTE_MAPPINGS = [
57
  # GET requests with path parameters go to ResourceTemplate
58
  RouteMap(
59
- methods=["GET"], pattern=r".*\{.*\}.*", route_type=RouteType.RESOURCE_TEMPLATE
60
  ),
61
  # GET requests without path parameters go to Resource
62
- RouteMap(methods=["GET"], pattern=r".*", route_type=RouteType.RESOURCE),
63
  # All other HTTP methods go to Tool
64
- RouteMap(
65
- methods=["POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"],
66
- pattern=r".*",
67
- route_type=RouteType.TOOL,
68
- ),
69
  ]
70
 
71
 
72
  def _determine_route_type(
73
  route: openapi.HTTPRoute,
74
  mappings: list[RouteMap],
75
- ) -> RouteType:
76
  """
77
  Determines the FastMCP component type based on the route and mappings.
78
 
@@ -81,7 +212,7 @@ def _determine_route_type(
81
  mappings: List of RouteMap objects in priority order
82
 
83
  Returns:
84
- RouteType for this route
85
  """
86
  # Check mappings in priority order (first match wins)
87
  for route_map in mappings:
@@ -94,13 +225,15 @@ def _determine_route_type(
94
  pattern_matches = re.search(route_map.pattern, route.path)
95
 
96
  if pattern_matches:
 
 
97
  logger.debug(
98
- f"Route {route.method} {route.path} matched mapping to {route_map.route_type.name}"
99
  )
100
- return route_map.route_type
101
 
102
  # Default fallback
103
- return RouteType.TOOL
104
 
105
 
106
  # Placeholder function to provide function metadata
@@ -555,13 +688,13 @@ class FastMCPOpenAPI(FastMCP):
555
  RouteMap(
556
  methods=["GET", "POST", "PATCH"],
557
  pattern=r".*/users/.*",
558
- route_type=RouteType.RESOURCE_TEMPLATE
559
  ),
560
  # Map all analytics endpoints to Tool
561
  RouteMap(
562
  methods=["GET"],
563
  pattern=r".*/analytics/.*",
564
- route_type=RouteType.TOOL
565
  ),
566
  ]
567
 
@@ -615,19 +748,19 @@ class FastMCPOpenAPI(FastMCP):
615
  path_name = "_".join(p for p in path_parts if not p.startswith("{"))
616
  operation_id = f"{route.method.lower()}_{path_name}"
617
 
618
- if route_type == RouteType.TOOL:
619
  self._create_openapi_tool(route, operation_id)
620
- elif route_type == RouteType.RESOURCE:
621
  self._create_openapi_resource(route, operation_id)
622
- elif route_type == RouteType.RESOURCE_TEMPLATE:
623
  self._create_openapi_template(route, operation_id)
624
- elif route_type == RouteType.PROMPT:
625
  # Not implemented yet
626
  logger.warning(
627
  f"PROMPT route type not implemented: {route.method} {route.path}"
628
  )
629
- elif route_type == RouteType.IGNORE:
630
- logger.info(f"Ignoring route: {route.method} {route.path}")
631
 
632
  logger.info(f"Created FastMCP OpenAPI server with {len(http_routes)} routes")
633
 
 
5
  import enum
6
  import json
7
  import re
8
+ import warnings
9
  from collections.abc import Callable
10
+ from dataclasses import dataclass, field
11
  from re import Pattern
12
  from typing import TYPE_CHECKING, Any, Literal
13
 
 
34
  HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]
35
 
36
 
37
+ class MCPType(enum.Enum):
38
+ """Type of FastMCP component to create from a route.
39
+
40
+ Enum values:
41
+ TOOL: Convert the route to a callable Tool
42
+ RESOURCE: Convert the route to a Resource (typically GET endpoints)
43
+ RESOURCE_TEMPLATE: Convert the route to a ResourceTemplate (typically GET with path params)
44
+ PROMPT: Convert the route to a Prompt (not yet implemented)
45
+ EXCLUDE: Exclude the route from being converted to any MCP component
46
+ IGNORE: Deprecated, use EXCLUDE instead
47
+ """
48
+
49
+ TOOL = "TOOL"
50
+ RESOURCE = "RESOURCE"
51
+ RESOURCE_TEMPLATE = "RESOURCE_TEMPLATE"
52
+ PROMPT = "PROMPT"
53
+ EXCLUDE = "EXCLUDE"
54
+
55
+
56
+ # Keep RouteType as an alias to MCPType for backward compatibility
57
  class RouteType(enum.Enum):
58
+ """
59
+ Deprecated: Use MCPType instead.
60
+
61
+ This enum is kept for backward compatibility and will be removed in a future version.
62
+ """
63
 
64
  TOOL = "TOOL"
65
  RESOURCE = "RESOURCE"
66
  RESOURCE_TEMPLATE = "RESOURCE_TEMPLATE"
67
  PROMPT = "PROMPT"
68
+ EXCLUDE = "EXCLUDE"
69
+ IGNORE = "IGNORE" # Deprecated, use EXCLUDE instead
70
+
71
+ def __new__(cls, value):
72
+ # Deprecated in 2.4.1
73
+ warnings.warn(
74
+ "RouteType is deprecated and will be removed in a future version. "
75
+ "Use MCPType instead.",
76
+ DeprecationWarning,
77
+ stacklevel=2,
78
+ )
79
+
80
+ # Add a specific warning for the deprecated IGNORE value
81
+ if value == "IGNORE":
82
+ warnings.warn(
83
+ "RouteType.IGNORE is deprecated and will be removed in a future version. "
84
+ "Use MCPType.EXCLUDE instead.",
85
+ DeprecationWarning,
86
+ stacklevel=2,
87
+ )
88
+
89
+ instance = object.__new__(cls)
90
+ instance._value_ = value
91
+ return instance
92
 
93
 
94
  @dataclass
95
  class RouteMap:
96
  """Mapping configuration for HTTP routes to FastMCP component types."""
97
 
98
+ methods: list[HttpMethod] | Literal["*"] = field(default="*")
99
+ pattern: Pattern[str] | str = field(default=r".*")
100
+ mcp_type: MCPType | None = field(default=None)
101
+ route_type: RouteType | MCPType | None = field(default=None)
102
+
103
+ def __post_init__(self):
104
+ """Validate and process the route map after initialization."""
105
+ # Handle backward compatibility for route_type
106
+ if self.mcp_type is None and self.route_type is not None:
107
+ warnings.warn(
108
+ "The 'route_type' parameter is deprecated and will be removed in a future version. "
109
+ "Use 'mcp_type' instead with the appropriate MCPType value.",
110
+ DeprecationWarning,
111
+ stacklevel=2,
112
+ )
113
+
114
+ # Check for the deprecated IGNORE value
115
+ if self.route_type == RouteType.IGNORE:
116
+ warnings.warn(
117
+ "RouteType.IGNORE is deprecated and will be removed in a future version. "
118
+ "Use MCPType.EXCLUDE instead.",
119
+ DeprecationWarning,
120
+ stacklevel=2,
121
+ )
122
+
123
+ # Convert from RouteType to MCPType if needed
124
+ if isinstance(self.route_type, RouteType):
125
+ route_type_name = self.route_type.name
126
+ if route_type_name == "IGNORE":
127
+ route_type_name = "EXCLUDE"
128
+ self.mcp_type = getattr(MCPType, route_type_name)
129
+ else:
130
+ self.mcp_type = self.route_type
131
+ elif self.mcp_type is None:
132
+ raise ValueError("`mcp_type` must be provided")
133
+
134
+ # Set route_type to match mcp_type for backward compatibility
135
+ if self.route_type is None:
136
+ self.route_type = self.mcp_type
137
+
138
+
139
+ # Common route map pattern functions
140
+ def EXCLUDE_ALL() -> RouteMap:
141
+ """
142
+ Create a RouteMap that excludes all routes that haven't been matched by earlier rules.
143
+
144
+ This is useful as the last route map to exclude any routes that don't match specific patterns.
145
+
146
+ Returns:
147
+ RouteMap: A route map that excludes all routes
148
+ """
149
+ return RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.EXCLUDE)
150
+
151
+
152
+ def ALL_TOOLS() -> RouteMap:
153
+ """
154
+ Create a RouteMap that converts all routes to tools that haven't been matched by earlier rules.
155
+
156
+ This is useful to replace the last item in the default route mappings to make all unmatched routes tools.
157
+
158
+ Returns:
159
+ RouteMap: A route map that converts all routes to tools
160
+ """
161
+ return RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)
162
+
163
+
164
+ def PATTERN_AS_TOOLS(pattern: str) -> RouteMap:
165
+ """
166
+ Create a RouteMap that converts routes matching a specific pattern to tools.
167
+
168
+ Args:
169
+ pattern: Regex pattern to match routes
170
+
171
+ Returns:
172
+ RouteMap: A route map that converts routes matching the pattern to tools
173
+ """
174
+ return RouteMap(methods="*", pattern=pattern, mcp_type=MCPType.TOOL)
175
+
176
+
177
+ def EXCLUDE_PATTERN(pattern: str) -> RouteMap:
178
+ """
179
+ Create a RouteMap that excludes routes matching a specific pattern.
180
+
181
+ Args:
182
+ pattern: Regex pattern to match routes to exclude
183
+
184
+ Returns:
185
+ RouteMap: A route map that excludes routes matching the pattern
186
+ """
187
+ return RouteMap(methods="*", pattern=pattern, mcp_type=MCPType.EXCLUDE)
188
 
189
 
190
  # Default route mappings as a list, where order determines priority
191
  DEFAULT_ROUTE_MAPPINGS = [
192
  # GET requests with path parameters go to ResourceTemplate
193
  RouteMap(
194
+ methods=["GET"], pattern=r".*\{.*\}.*", mcp_type=MCPType.RESOURCE_TEMPLATE
195
  ),
196
  # GET requests without path parameters go to Resource
197
+ RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE),
198
  # All other HTTP methods go to Tool
199
+ ALL_TOOLS(),
 
 
 
 
200
  ]
201
 
202
 
203
  def _determine_route_type(
204
  route: openapi.HTTPRoute,
205
  mappings: list[RouteMap],
206
+ ) -> MCPType:
207
  """
208
  Determines the FastMCP component type based on the route and mappings.
209
 
 
212
  mappings: List of RouteMap objects in priority order
213
 
214
  Returns:
215
+ MCPType for this route
216
  """
217
  # Check mappings in priority order (first match wins)
218
  for route_map in mappings:
 
225
  pattern_matches = re.search(route_map.pattern, route.path)
226
 
227
  if pattern_matches:
228
+ # We know mcp_type is not None here due to post_init validation
229
+ assert route_map.mcp_type is not None
230
  logger.debug(
231
+ f"Route {route.method} {route.path} matched mapping to {route_map.mcp_type.name}"
232
  )
233
+ return route_map.mcp_type
234
 
235
  # Default fallback
236
+ return MCPType.TOOL
237
 
238
 
239
  # Placeholder function to provide function metadata
 
688
  RouteMap(
689
  methods=["GET", "POST", "PATCH"],
690
  pattern=r".*/users/.*",
691
+ mcp_type=MCPType.RESOURCE_TEMPLATE
692
  ),
693
  # Map all analytics endpoints to Tool
694
  RouteMap(
695
  methods=["GET"],
696
  pattern=r".*/analytics/.*",
697
+ mcp_type=MCPType.TOOL
698
  ),
699
  ]
700
 
 
748
  path_name = "_".join(p for p in path_parts if not p.startswith("{"))
749
  operation_id = f"{route.method.lower()}_{path_name}"
750
 
751
+ if route_type == MCPType.TOOL:
752
  self._create_openapi_tool(route, operation_id)
753
+ elif route_type == MCPType.RESOURCE:
754
  self._create_openapi_resource(route, operation_id)
755
+ elif route_type == MCPType.RESOURCE_TEMPLATE:
756
  self._create_openapi_template(route, operation_id)
757
+ elif route_type == MCPType.PROMPT:
758
  # Not implemented yet
759
  logger.warning(
760
  f"PROMPT route type not implemented: {route.method} {route.path}"
761
  )
762
+ elif route_type == MCPType.EXCLUDE:
763
+ logger.info(f"Excluding route: {route.method} {route.path}")
764
 
765
  logger.info(f"Created FastMCP OpenAPI server with {len(http_routes)} routes")
766
 
tests/deprecated/test_route_type_ignore.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the deprecated RouteType.IGNORE."""
2
+
3
+ import warnings
4
+
5
+ import httpx
6
+ import pytest
7
+
8
+ from fastmcp.server.openapi import (
9
+ FastMCPOpenAPI,
10
+ MCPType,
11
+ RouteMap,
12
+ RouteType,
13
+ )
14
+
15
+
16
+ def test_route_type_ignore_deprecation_warning():
17
+ """Test that using RouteType.IGNORE emits a deprecation warning."""
18
+ # Let's manually capture the warnings
19
+
20
+ # Record all warnings
21
+ with warnings.catch_warnings(record=True) as recorded:
22
+ # Make sure warnings are always triggered
23
+ warnings.simplefilter("always")
24
+
25
+ # Create a RouteMap with RouteType.IGNORE
26
+ route_map = RouteMap(
27
+ methods=["GET"], pattern=r"^/analytics$", route_type=RouteType.IGNORE
28
+ )
29
+
30
+ # Check for the expected warnings in the recorded warnings
31
+ route_type_warning = False
32
+ ignore_warning = False
33
+
34
+ for w in recorded:
35
+ if issubclass(w.category, DeprecationWarning):
36
+ message = str(w.message)
37
+ if "route_type' parameter is deprecated" in message:
38
+ route_type_warning = True
39
+ if "RouteType.IGNORE is deprecated" in message:
40
+ ignore_warning = True
41
+
42
+ # Make sure both warnings were triggered
43
+ assert route_type_warning, "Missing 'route_type' deprecation warning"
44
+ assert ignore_warning, "Missing 'RouteType.IGNORE' deprecation warning"
45
+
46
+ # Verify that RouteType.IGNORE was converted to MCPType.EXCLUDE
47
+ assert route_map.mcp_type == MCPType.EXCLUDE
48
+
49
+
50
+ class TestRouteTypeIgnoreDeprecation:
51
+ """Test class for the deprecated RouteType.IGNORE."""
52
+
53
+ @pytest.fixture
54
+ def basic_openapi_spec(self) -> dict:
55
+ """Create a simple OpenAPI spec for testing."""
56
+ return {
57
+ "openapi": "3.0.0",
58
+ "info": {"title": "Test API", "version": "1.0.0"},
59
+ "paths": {
60
+ "/items": {
61
+ "get": {
62
+ "operationId": "get_items",
63
+ "summary": "Get all items",
64
+ "responses": {"200": {"description": "Success"}},
65
+ }
66
+ },
67
+ "/analytics": {
68
+ "get": {
69
+ "operationId": "get_analytics",
70
+ "summary": "Get analytics data",
71
+ "responses": {"200": {"description": "Success"}},
72
+ }
73
+ },
74
+ },
75
+ }
76
+
77
+ @pytest.fixture
78
+ async def mock_client(self) -> httpx.AsyncClient:
79
+ """Create a mock client for testing."""
80
+
81
+ async def _responder(request):
82
+ return httpx.Response(200, json={"success": True})
83
+
84
+ return httpx.AsyncClient(transport=httpx.MockTransport(_responder))
85
+
86
+ async def test_route_type_ignore_conversion(self, basic_openapi_spec, mock_client):
87
+ """Test that routes with RouteType.IGNORE are properly excluded."""
88
+ # Capture the deprecation warning without checking the exact message
89
+ with pytest.warns(DeprecationWarning):
90
+ server = FastMCPOpenAPI(
91
+ openapi_spec=basic_openapi_spec,
92
+ client=mock_client,
93
+ route_maps=[
94
+ # Use the deprecated RouteType.IGNORE
95
+ RouteMap(
96
+ methods=["GET"],
97
+ pattern=r"^/analytics$",
98
+ route_type=RouteType.IGNORE,
99
+ ),
100
+ # Make everything else a resource
101
+ RouteMap(
102
+ methods=["GET"], pattern=r".*", route_type=RouteType.RESOURCE
103
+ ),
104
+ ],
105
+ )
106
+
107
+ # Check that the analytics route was excluded (converted from IGNORE to EXCLUDE)
108
+ resources = await server.get_resources()
109
+ resource_uris = [str(r.uri) for r in resources.values()]
110
+
111
+ # Analytics should be excluded
112
+ assert "resource://openapi/get_items" in resource_uris
113
+ assert "resource://openapi/get_analytics" not in resource_uris
tests/server/test_openapi.py CHANGED
@@ -2152,3 +2152,71 @@ class TestAllRoutesAsTools:
2152
  )
2153
  ],
2154
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2152
  )
2153
  ],
2154
  )
2155
+
2156
+
2157
+ class TestRouteTypeExclude:
2158
+ @pytest.fixture
2159
+ def basic_openapi_spec(self) -> dict:
2160
+ return {
2161
+ "openapi": "3.0.0",
2162
+ "info": {"title": "Test API", "version": "1.0.0"},
2163
+ "paths": {
2164
+ "/items": {
2165
+ "get": {
2166
+ "operationId": "get_items",
2167
+ "summary": "Get all items",
2168
+ "responses": {"200": {"description": "Success"}},
2169
+ }
2170
+ },
2171
+ "/users": {
2172
+ "get": {
2173
+ "operationId": "get_users",
2174
+ "summary": "Get all users",
2175
+ "responses": {"200": {"description": "Success"}},
2176
+ }
2177
+ },
2178
+ "/analytics": {
2179
+ "get": {
2180
+ "operationId": "get_analytics",
2181
+ "summary": "Get analytics data",
2182
+ "responses": {"200": {"description": "Success"}},
2183
+ }
2184
+ },
2185
+ },
2186
+ }
2187
+
2188
+ @pytest.fixture
2189
+ async def mock_client(self) -> httpx.AsyncClient:
2190
+ async def _responder(request):
2191
+ return httpx.Response(200, json={"success": True})
2192
+
2193
+ return httpx.AsyncClient(transport=httpx.MockTransport(_responder))
2194
+
2195
+ async def test_exclude_routes(self, basic_openapi_spec, mock_client):
2196
+ # Create a server with custom mappings that exclude specific routes
2197
+ server = FastMCPOpenAPI(
2198
+ openapi_spec=basic_openapi_spec,
2199
+ client=mock_client,
2200
+ route_maps=[
2201
+ # Exclude analytics endpoints
2202
+ RouteMap(
2203
+ methods=["GET"],
2204
+ pattern=r"^/analytics$",
2205
+ route_type=RouteType.IGNORE,
2206
+ ),
2207
+ # Make everything else a resource
2208
+ RouteMap(methods=["GET"], pattern=r".*", route_type=RouteType.RESOURCE),
2209
+ ],
2210
+ )
2211
+
2212
+ # Check that resources were created for non-excluded routes
2213
+ resources = await server.get_resources()
2214
+ resource_uris = [str(r.uri) for r in resources.values()]
2215
+
2216
+ # The /analytics endpoint should be excluded
2217
+ assert "resource://openapi/get_items" in resource_uris
2218
+ assert "resource://openapi/get_users" in resource_uris
2219
+ assert "resource://openapi/get_analytics" not in resource_uris
2220
+
2221
+ # Should only have 2 resources (analytics is excluded)
2222
+ assert len(resources) == 2
tests/server/test_route_map_shortcuts.py ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the route map shortcut functions."""
2
+
3
+ import httpx
4
+ import pytest
5
+
6
+ from fastmcp.server.openapi import (
7
+ ALL_TOOLS,
8
+ EXCLUDE_ALL,
9
+ EXCLUDE_PATTERN,
10
+ PATTERN_AS_TOOLS,
11
+ FastMCPOpenAPI,
12
+ MCPType,
13
+ RouteMap,
14
+ RouteType,
15
+ )
16
+
17
+
18
+ class TestRouteMapShortcuts:
19
+ """Tests for the route map shortcut functions."""
20
+
21
+ def test_functions_return_correct_route_maps(self):
22
+ """Test that each shortcut function returns a RouteMap with the expected properties."""
23
+ # Test EXCLUDE_ALL
24
+ exclude_all = EXCLUDE_ALL()
25
+ assert isinstance(exclude_all, RouteMap)
26
+ assert exclude_all.methods == "*"
27
+ assert exclude_all.pattern == ".*"
28
+ assert exclude_all.mcp_type == MCPType.EXCLUDE
29
+
30
+ # Test ALL_TOOLS
31
+ all_tools = ALL_TOOLS()
32
+ assert isinstance(all_tools, RouteMap)
33
+ assert all_tools.methods == "*"
34
+ assert all_tools.pattern == ".*"
35
+ assert all_tools.mcp_type == MCPType.TOOL
36
+
37
+ # Test PATTERN_AS_TOOLS
38
+ pattern = r"^/api/.*"
39
+ pattern_as_tools = PATTERN_AS_TOOLS(pattern)
40
+ assert isinstance(pattern_as_tools, RouteMap)
41
+ assert pattern_as_tools.methods == "*"
42
+ assert pattern_as_tools.pattern == pattern
43
+ assert pattern_as_tools.mcp_type == MCPType.TOOL
44
+
45
+ # Test EXCLUDE_PATTERN
46
+ pattern = r"^/admin/.*"
47
+ exclude_pattern = EXCLUDE_PATTERN(pattern)
48
+ assert isinstance(exclude_pattern, RouteMap)
49
+ assert exclude_pattern.methods == "*"
50
+ assert exclude_pattern.pattern == pattern
51
+ assert exclude_pattern.mcp_type == MCPType.EXCLUDE
52
+
53
+ def test_backward_compatibility(self):
54
+ """Test that backward compatibility with RouteType and route_type works."""
55
+ # Test creating a RouteMap with route_type
56
+ with pytest.warns(DeprecationWarning):
57
+ route_map = RouteMap(
58
+ methods=["GET"], pattern=r".*", route_type=RouteType.TOOL
59
+ )
60
+ assert route_map.mcp_type == MCPType.TOOL
61
+
62
+ # Test accessing fields on RouteType directly
63
+ # Note: importing RouteType already causes the deprecation warning,
64
+ # so we don't need to check for it again here
65
+ rt = RouteType.RESOURCE
66
+ assert rt.value == "RESOURCE"
67
+ assert rt.name == "RESOURCE"
68
+
69
+
70
+ class TestRouteMapShortcutsIntegration:
71
+ """Integration tests for the route map shortcut functions with FastMCPOpenAPI."""
72
+
73
+ @pytest.fixture
74
+ def basic_openapi_spec(self) -> dict:
75
+ """Create a simple OpenAPI spec for testing."""
76
+ return {
77
+ "openapi": "3.0.0",
78
+ "info": {"title": "Test API", "version": "1.0.0"},
79
+ "paths": {
80
+ "/items": {
81
+ "get": {
82
+ "operationId": "get_items",
83
+ "summary": "Get all items",
84
+ "responses": {"200": {"description": "Success"}},
85
+ },
86
+ "post": {
87
+ "operationId": "create_item",
88
+ "summary": "Create an item",
89
+ "responses": {"201": {"description": "Created"}},
90
+ },
91
+ },
92
+ "/users": {
93
+ "get": {
94
+ "operationId": "get_users",
95
+ "summary": "Get all users",
96
+ "responses": {"200": {"description": "Success"}},
97
+ },
98
+ },
99
+ "/admin": {
100
+ "get": {
101
+ "operationId": "get_admin",
102
+ "summary": "Admin endpoint",
103
+ "responses": {"200": {"description": "Success"}},
104
+ },
105
+ },
106
+ "/items/{item_id}": {
107
+ "get": {
108
+ "operationId": "get_item",
109
+ "summary": "Get an item by ID",
110
+ "parameters": [
111
+ {
112
+ "name": "item_id",
113
+ "in": "path",
114
+ "required": True,
115
+ "schema": {"type": "string"},
116
+ }
117
+ ],
118
+ "responses": {"200": {"description": "Success"}},
119
+ },
120
+ },
121
+ },
122
+ }
123
+
124
+ @pytest.fixture
125
+ async def mock_client(self) -> httpx.AsyncClient:
126
+ """Create a mock client for testing."""
127
+
128
+ async def _responder(request):
129
+ return httpx.Response(200, json={"success": True})
130
+
131
+ return httpx.AsyncClient(transport=httpx.MockTransport(_responder))
132
+
133
+ async def test_all_tools(self, basic_openapi_spec, mock_client):
134
+ """Test using ALL_TOOLS() to convert all routes to tools."""
135
+ server = FastMCPOpenAPI(
136
+ openapi_spec=basic_openapi_spec,
137
+ client=mock_client,
138
+ route_maps=[ALL_TOOLS()],
139
+ )
140
+
141
+ # Check that all routes are tools
142
+ tools = await server.get_tools()
143
+ resources = await server.get_resources()
144
+ templates = await server.get_resource_templates()
145
+
146
+ # All 5 routes should be tools
147
+ assert len(tools) == 5
148
+ assert len(resources) == 0
149
+ assert len(templates) == 0
150
+
151
+ # Check that all expected tools exist
152
+ tool_names = [t.name for t in tools.values()]
153
+ assert "get_items" in tool_names
154
+ assert "create_item" in tool_names
155
+ assert "get_users" in tool_names
156
+ assert "get_admin" in tool_names
157
+ assert "get_item" in tool_names
158
+
159
+ async def test_exclude_pattern(self, basic_openapi_spec, mock_client):
160
+ """Test using EXCLUDE_PATTERN() to exclude specific routes."""
161
+ server = FastMCPOpenAPI(
162
+ openapi_spec=basic_openapi_spec,
163
+ client=mock_client,
164
+ route_maps=[
165
+ # Exclude admin endpoints
166
+ EXCLUDE_PATTERN(r"^/admin"),
167
+ # Make everything else a tool
168
+ ALL_TOOLS(),
169
+ ],
170
+ )
171
+
172
+ # Check that admin route is excluded
173
+ tools = await server.get_tools()
174
+ tool_names = [t.name for t in tools.values()]
175
+
176
+ # All routes except admin should be tools
177
+ assert "get_items" in tool_names
178
+ assert "create_item" in tool_names
179
+ assert "get_users" in tool_names
180
+ assert "get_item" in tool_names
181
+ assert "get_admin" not in tool_names # This should be excluded
182
+
183
+ async def test_pattern_as_tools(self, basic_openapi_spec, mock_client):
184
+ """Test using PATTERN_AS_TOOLS() to convert routes matching a pattern to tools."""
185
+ server = FastMCPOpenAPI(
186
+ openapi_spec=basic_openapi_spec,
187
+ client=mock_client,
188
+ route_maps=[
189
+ # Make /items routes tools regardless of method
190
+ PATTERN_AS_TOOLS(r"^/items"),
191
+ # Make everything else a resource
192
+ RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.RESOURCE),
193
+ ],
194
+ )
195
+
196
+ # Check that /items routes are tools
197
+ tools = await server.get_tools()
198
+ tool_names = [t.name for t in tools.values()]
199
+ assert "get_items" in tool_names
200
+ assert "create_item" in tool_names
201
+ assert "get_item" in tool_names
202
+
203
+ # Check that other routes are resources
204
+ resources = await server.get_resources()
205
+ resource_names = [r.name for r in resources.values()]
206
+ assert "get_users" in resource_names
207
+ assert "get_admin" in resource_names