Spaces:
Running
Running
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 2713 |
-
create_item_tool =
|
| 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 =
|
| 2721 |
-
get_items_resource =
|
| 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 =
|
| 2729 |
-
get_item_template =
|
| 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.
|
| 2758 |
-
create_item_tool =
|
| 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 =
|
| 2766 |
-
get_items_resource =
|
| 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 =
|
| 2774 |
-
get_item_template =
|
| 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.
|
| 2804 |
-
create_item_tool =
|
| 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 =
|
| 2812 |
-
get_items_resource =
|
| 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
|