Jeremiah Lowin commited on
Commit
5496833
·
unverified ·
2 Parent(s): 05164d12b93ce5

Merge pull request #792 from jlowin/tags

Browse files
docs/servers/openapi.mdx CHANGED
@@ -51,6 +51,7 @@ Each `RouteMap` specifies a combination of methods, patterns, and tags, as well
51
  - **Pattern**: Regex pattern to match the route path (e.g. `r"^/users/.*"` or `r".*"` for all)
52
  - **Tags**: A set of OpenAPI tags that must all be present. An empty set (`{}`) means no tag filtering, so the route matches regardless of its tags.
53
  - **MCP type**: What MCP component type to create (`TOOL`, `RESOURCE`, `RESOURCE_TEMPLATE`, or `EXCLUDE`)
 
54
 
55
  Here is FastMCP's default rule:
56
 
@@ -206,9 +207,76 @@ mcp = FastMCP.from_openapi(
206
 
207
  ## Customizing MCP Components
208
 
 
209
 
 
210
 
211
- ### Component Names
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
 
213
  <VersionBadge version="2.5.0" />
214
 
@@ -421,10 +489,16 @@ mcp = FastMCP.from_fastapi(
421
  app=app,
422
  name="My Custom Server",
423
  timeout=5.0,
 
424
  mcp_names={"operationId": "friendly_name"}, # Custom component names
425
  route_maps=[
426
- # Admin endpoints become tools
427
- RouteMap(methods="*", pattern=r"^/admin/.*", mcp_type=MCPType.TOOL),
 
 
 
 
 
428
  # Internal endpoints are excluded
429
  RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.EXCLUDE, tags={"internal"}),
430
  ],
 
51
  - **Pattern**: Regex pattern to match the route path (e.g. `r"^/users/.*"` or `r".*"` for all)
52
  - **Tags**: A set of OpenAPI tags that must all be present. An empty set (`{}`) means no tag filtering, so the route matches regardless of its tags.
53
  - **MCP type**: What MCP component type to create (`TOOL`, `RESOURCE`, `RESOURCE_TEMPLATE`, or `EXCLUDE`)
54
+ - **MCP tags** A set of custom tags to add to components created from matching routes
55
 
56
  Here is FastMCP's default rule:
57
 
 
207
 
208
  ## Customizing MCP Components
209
 
210
+ ### Tags
211
 
212
+ <VersionBadge version="2.8.0" />
213
 
214
+ FastMCP provides several ways to add tags to your MCP components, allowing you to categorize and organize them for better discoverability and filtering. Tags are combined from multiple sources to create the final set of tags on each component.
215
+
216
+ #### RouteMap Tags
217
+
218
+ You can add custom tags to components created from specific routes using the `mcp_tags` parameter in `RouteMap`. These tags will be applied to all components created from routes that match that particular route map.
219
+
220
+ ```python {12, 20, 28}
221
+ from fastmcp import FastMCP
222
+ from fastmcp.server.openapi import RouteMap, MCPType
223
+
224
+ mcp = FastMCP.from_openapi(
225
+ ...,
226
+ route_maps=[
227
+ # Add custom tags to all POST endpoints
228
+ RouteMap(
229
+ methods=["POST"],
230
+ pattern=r".*",
231
+ mcp_type=MCPType.TOOL,
232
+ mcp_tags={"write-operation", "api-mutation"}
233
+ ),
234
+
235
+ # Add different tags to detail view endpoints
236
+ RouteMap(
237
+ methods=["GET"],
238
+ pattern=r".*\{.*\}.*",
239
+ mcp_type=MCPType.RESOURCE_TEMPLATE,
240
+ mcp_tags={"detail-view", "parameterized"}
241
+ ),
242
+
243
+ # Add tags to list endpoints
244
+ RouteMap(
245
+ methods=["GET"],
246
+ pattern=r".*",
247
+ mcp_type=MCPType.RESOURCE,
248
+ mcp_tags={"list-data", "collection"}
249
+ ),
250
+ ],
251
+ )
252
+ ```
253
+
254
+ #### Global Tags
255
+
256
+ You can add tags to **all** components by providing a `tags` parameter when creating your FastMCP server with `from_openapi` or `from_fastapi`. These global tags will be applied to every component created from your OpenAPI specification.
257
+
258
+ <CodeGroup>
259
+ ```python {6} from_openapi()
260
+ from fastmcp import FastMCP
261
+
262
+ mcp = FastMCP.from_openapi(
263
+ openapi_spec=spec,
264
+ client=client,
265
+ tags={"api-v2", "production", "external"}
266
+ )
267
+ ```
268
+ ```python {5} from_fastapi()
269
+ from fastmcp import FastMCP
270
+
271
+ mcp = FastMCP.from_fastapi(
272
+ app=app,
273
+ tags={"internal-api", "microservice"}
274
+ )
275
+ ```
276
+ </CodeGroup>
277
+
278
+
279
+ ### Names
280
 
281
  <VersionBadge version="2.5.0" />
282
 
 
489
  app=app,
490
  name="My Custom Server",
491
  timeout=5.0,
492
+ tags={"api-v1", "fastapi"}, # Global tags for all components
493
  mcp_names={"operationId": "friendly_name"}, # Custom component names
494
  route_maps=[
495
+ # Admin endpoints become tools with custom tags
496
+ RouteMap(
497
+ methods="*",
498
+ pattern=r"^/admin/.*",
499
+ mcp_type=MCPType.TOOL,
500
+ mcp_tags={"admin", "privileged"}
501
+ ),
502
  # Internal endpoints are excluded
503
  RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.EXCLUDE, tags={"internal"}),
504
  ],
src/fastmcp/server/server.py CHANGED
@@ -121,7 +121,6 @@ class FastMCP(Generic[LifespanResultT]):
121
  ]
122
  | None
123
  ) = None,
124
- tags: set[str] | None = None,
125
  tool_serializer: Callable[[Any], str] | None = None,
126
  cache_expiration_seconds: float | None = None,
127
  on_duplicate_tools: DuplicateBehavior | None = None,
@@ -152,8 +151,6 @@ class FastMCP(Generic[LifespanResultT]):
152
  resource_prefix_format or fastmcp.settings.resource_prefix_format
153
  )
154
 
155
- self.tags: set[str] = tags or set()
156
-
157
  self._cache = TimedCache(
158
  expiration=datetime.timedelta(seconds=cache_expiration_seconds or 0)
159
  )
@@ -1561,6 +1558,7 @@ class FastMCP(Generic[LifespanResultT]):
1561
  route_map_fn: OpenAPIRouteMapFn | None = None,
1562
  mcp_component_fn: OpenAPIComponentFn | None = None,
1563
  mcp_names: dict[str, str] | None = None,
 
1564
  **settings: Any,
1565
  ) -> FastMCPOpenAPI:
1566
  """
@@ -1575,6 +1573,7 @@ class FastMCP(Generic[LifespanResultT]):
1575
  route_map_fn=route_map_fn,
1576
  mcp_component_fn=mcp_component_fn,
1577
  mcp_names=mcp_names,
 
1578
  **settings,
1579
  )
1580
 
@@ -1588,6 +1587,7 @@ class FastMCP(Generic[LifespanResultT]):
1588
  mcp_component_fn: OpenAPIComponentFn | None = None,
1589
  mcp_names: dict[str, str] | None = None,
1590
  httpx_client_kwargs: dict[str, Any] | None = None,
 
1591
  **settings: Any,
1592
  ) -> FastMCPOpenAPI:
1593
  """
@@ -1615,6 +1615,7 @@ class FastMCP(Generic[LifespanResultT]):
1615
  route_map_fn=route_map_fn,
1616
  mcp_component_fn=mcp_component_fn,
1617
  mcp_names=mcp_names,
 
1618
  **settings,
1619
  )
1620
 
 
121
  ]
122
  | None
123
  ) = None,
 
124
  tool_serializer: Callable[[Any], str] | None = None,
125
  cache_expiration_seconds: float | None = None,
126
  on_duplicate_tools: DuplicateBehavior | None = None,
 
151
  resource_prefix_format or fastmcp.settings.resource_prefix_format
152
  )
153
 
 
 
154
  self._cache = TimedCache(
155
  expiration=datetime.timedelta(seconds=cache_expiration_seconds or 0)
156
  )
 
1558
  route_map_fn: OpenAPIRouteMapFn | None = None,
1559
  mcp_component_fn: OpenAPIComponentFn | None = None,
1560
  mcp_names: dict[str, str] | None = None,
1561
+ tags: set[str] | None = None,
1562
  **settings: Any,
1563
  ) -> FastMCPOpenAPI:
1564
  """
 
1573
  route_map_fn=route_map_fn,
1574
  mcp_component_fn=mcp_component_fn,
1575
  mcp_names=mcp_names,
1576
+ tags=tags,
1577
  **settings,
1578
  )
1579
 
 
1587
  mcp_component_fn: OpenAPIComponentFn | None = None,
1588
  mcp_names: dict[str, str] | None = None,
1589
  httpx_client_kwargs: dict[str, Any] | None = None,
1590
+ tags: set[str] | None = None,
1591
  **settings: Any,
1592
  ) -> FastMCPOpenAPI:
1593
  """
 
1615
  route_map_fn=route_map_fn,
1616
  mcp_component_fn=mcp_component_fn,
1617
  mcp_names=mcp_names,
1618
+ tags=tags,
1619
  **settings,
1620
  )
1621
 
tests/deprecated/test_settings.py CHANGED
@@ -175,7 +175,6 @@ class TestDeprecatedServerInitKwargs:
175
  server = FastMCP(
176
  name="TestServer",
177
  instructions="Test instructions",
178
- tags={"test", "server"},
179
  cache_expiration_seconds=60.0,
180
  on_duplicate_tools="warn",
181
  on_duplicate_resources="error",
@@ -193,7 +192,6 @@ class TestDeprecatedServerInitKwargs:
193
  # Verify server was created successfully
194
  assert server.name == "TestServer"
195
  assert server.instructions == "Test instructions"
196
- assert server.tags == {"test", "server"}
197
 
198
  def test_none_values_no_warnings(self):
199
  """Test that None values for deprecated kwargs don't raise warnings."""
 
175
  server = FastMCP(
176
  name="TestServer",
177
  instructions="Test instructions",
 
178
  cache_expiration_seconds=60.0,
179
  on_duplicate_tools="warn",
180
  on_duplicate_resources="error",
 
192
  # Verify server was created successfully
193
  assert server.name == "TestServer"
194
  assert server.instructions == "Test instructions"
 
195
 
196
  def test_none_values_no_warnings(self):
197
  """Test that None values for deprecated kwargs don't raise warnings."""
tests/server/openapi/test_openapi.py CHANGED
@@ -2709,25 +2709,22 @@ class TestGlobalTagsParameter:
2709
  )
2710
 
2711
  # Check tool has both original and global tags
2712
- tools = server._tool_manager.list_tools()
2713
- create_item_tool = next((t for t in tools if "create_item" in t.name), None)
2714
- assert create_item_tool is not None
2715
  assert "items" in create_item_tool.tags # Original OpenAPI tag
2716
  assert "global" in create_item_tool.tags # Global tag
2717
  assert "api-v1" in create_item_tool.tags # Global tag
2718
 
2719
  # Check resource has both original and global tags
2720
- resources = list(server._resource_manager.get_resources().values())
2721
- get_items_resource = next((r for r in resources if "get_items" in r.name), None)
2722
- assert get_items_resource is not None
2723
  assert "items" in get_items_resource.tags # Original OpenAPI tag
2724
  assert "global" in get_items_resource.tags # Global tag
2725
  assert "api-v1" in get_items_resource.tags # Global tag
2726
 
2727
  # Check resource template has both original and global tags
2728
- templates = list(server._resource_manager.get_templates().values())
2729
- get_item_template = next((t for t in templates if "get_item" in t.name), None)
2730
- assert get_item_template is not None
2731
  assert "items" in get_item_template.tags # Original OpenAPI tag
2732
  assert "global" in get_item_template.tags # Global tag
2733
  assert "api-v1" in get_item_template.tags # Global tag
@@ -2754,25 +2751,22 @@ class TestGlobalTagsParameter:
2754
  )
2755
 
2756
  # Check tool has both original and global tags
2757
- tools = server._tool_manager.list_tools()
2758
- create_item_tool = next((t for t in tools if "create_item" in t.name), None)
2759
- assert create_item_tool is not None
2760
  assert "items" in create_item_tool.tags # Original OpenAPI tag
2761
  assert "openapi-global" in create_item_tool.tags # Global tag
2762
  assert "service" in create_item_tool.tags # Global tag
2763
 
2764
  # Check resource has both original and global tags
2765
- resources = list(server._resource_manager.get_resources().values())
2766
- get_items_resource = next((r for r in resources if "get_items" in r.name), None)
2767
- assert get_items_resource is not None
2768
  assert "items" in get_items_resource.tags # Original OpenAPI tag
2769
  assert "openapi-global" in get_items_resource.tags # Global tag
2770
  assert "service" in get_items_resource.tags # Global tag
2771
 
2772
  # Check resource template has both original and global tags
2773
- templates = list(server._resource_manager.get_templates().values())
2774
- get_item_template = next((t for t in templates if "get_item" in t.name), None)
2775
- assert get_item_template is not None
2776
  assert "items" in get_item_template.tags # Original OpenAPI tag
2777
  assert "openapi-global" in get_item_template.tags # Global tag
2778
  assert "service" in get_item_template.tags # Global tag
@@ -2800,17 +2794,15 @@ class TestGlobalTagsParameter:
2800
  )
2801
 
2802
  # Check that all three types of tags are present on the tool
2803
- tools = server._tool_manager.list_tools()
2804
- create_item_tool = next((t for t in tools if "create_item" in t.name), None)
2805
- assert create_item_tool is not None
2806
  assert "items" in create_item_tool.tags # Original OpenAPI tag
2807
  assert "global" in create_item_tool.tags # Global tag
2808
  assert "route-specific" in create_item_tool.tags # RouteMap mcp_tag
2809
 
2810
  # Check that resource only has OpenAPI and global tags (no route-specific since different RouteMap)
2811
- resources = list(server._resource_manager.get_resources().values())
2812
- get_items_resource = next((r for r in resources if "get_items" in r.name), None)
2813
- assert get_items_resource is not None
2814
  assert "items" in get_items_resource.tags # Original OpenAPI tag
2815
  assert "global" in get_items_resource.tags # Global tag
2816
  assert "route-specific" not in get_items_resource.tags # Not from this RouteMap
 
2709
  )
2710
 
2711
  # Check tool has both original and global tags
2712
+ tools = await server.get_tools()
2713
+ create_item_tool = tools["create_item_items_post"]
 
2714
  assert "items" in create_item_tool.tags # Original OpenAPI tag
2715
  assert "global" in create_item_tool.tags # Global tag
2716
  assert "api-v1" in create_item_tool.tags # Global tag
2717
 
2718
  # Check resource has both original and global tags
2719
+ resources = await server.get_resources()
2720
+ get_items_resource = resources["resource://get_items_items_get"]
 
2721
  assert "items" in get_items_resource.tags # Original OpenAPI tag
2722
  assert "global" in get_items_resource.tags # Global tag
2723
  assert "api-v1" in get_items_resource.tags # Global tag
2724
 
2725
  # Check resource template has both original and global tags
2726
+ templates = await server.get_resource_templates()
2727
+ get_item_template = templates["resource://get_item_items/{item_id}"]
 
2728
  assert "items" in get_item_template.tags # Original OpenAPI tag
2729
  assert "global" in get_item_template.tags # Global tag
2730
  assert "api-v1" in get_item_template.tags # Global tag
 
2751
  )
2752
 
2753
  # Check tool has both original and global tags
2754
+ tools = await server.get_tools()
2755
+ create_item_tool = tools["create_item_items_post"]
 
2756
  assert "items" in create_item_tool.tags # Original OpenAPI tag
2757
  assert "openapi-global" in create_item_tool.tags # Global tag
2758
  assert "service" in create_item_tool.tags # Global tag
2759
 
2760
  # Check resource has both original and global tags
2761
+ resources = await server.get_resources()
2762
+ get_items_resource = resources["resource://get_items_items_get"]
 
2763
  assert "items" in get_items_resource.tags # Original OpenAPI tag
2764
  assert "openapi-global" in get_items_resource.tags # Global tag
2765
  assert "service" in get_items_resource.tags # Global tag
2766
 
2767
  # Check resource template has both original and global tags
2768
+ templates = await server.get_resource_templates()
2769
+ get_item_template = templates["resource://get_item_items/{item_id}"]
 
2770
  assert "items" in get_item_template.tags # Original OpenAPI tag
2771
  assert "openapi-global" in get_item_template.tags # Global tag
2772
  assert "service" in get_item_template.tags # Global tag
 
2794
  )
2795
 
2796
  # Check that all three types of tags are present on the tool
2797
+ tools = await server.get_tools()
2798
+ create_item_tool = tools["create_item_items_post"]
 
2799
  assert "items" in create_item_tool.tags # Original OpenAPI tag
2800
  assert "global" in create_item_tool.tags # Global tag
2801
  assert "route-specific" in create_item_tool.tags # RouteMap mcp_tag
2802
 
2803
  # Check that resource only has OpenAPI and global tags (no route-specific since different RouteMap)
2804
+ resources = await server.get_resources()
2805
+ get_items_resource = resources["resource://get_items_items_get"]
 
2806
  assert "items" in get_items_resource.tags # Original OpenAPI tag
2807
  assert "global" in get_items_resource.tags # Global tag
2808
  assert "route-specific" not in get_items_resource.tags # Not from this RouteMap