Rename _fastmcp metadata namespace to fastmcp and make non-optional (#2895)

This commit is contained in:
Jeremiah Lowin 2026-01-16 21:35:53 -05:00 committed by GitHub
commit 53e220a99e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 180 additions and 235 deletions

View file

@ -27,7 +27,7 @@ async with client:
print(f"Arguments: {[arg.name for arg in prompt.arguments]}")
# Access tags and other metadata
if prompt.meta:
fastmcp_meta = prompt.meta.get('_fastmcp', {})
fastmcp_meta = prompt.meta.get('fastmcp', {})
print(f"Tags: {fastmcp_meta.get('tags', [])}")
```
@ -45,15 +45,15 @@ async with client:
analysis_prompts = [
prompt for prompt in prompts
if prompt.meta and
prompt.meta.get('_fastmcp', {}) and
'analysis' in prompt.meta.get('_fastmcp', {}).get('tags', [])
prompt.meta.get('fastmcp', {}) and
'analysis' in prompt.meta.get('fastmcp', {}).get('tags', [])
]
print(f"Found {len(analysis_prompts)} analysis prompts")
```
<Note>
The `meta` field is part of the standard MCP specification. FastMCP servers include tags and other metadata within a `_fastmcp` namespace (e.g., `meta._fastmcp.tags`) to avoid conflicts with user-defined metadata. This behavior can be controlled with the server's `include_fastmcp_meta` setting - when disabled, the `_fastmcp` namespace won't be included. Other MCP server implementations may not provide this metadata structure.
The `meta` field is part of the standard MCP specification. FastMCP servers always include tags and other metadata within a `fastmcp` namespace (e.g., `meta.fastmcp.tags`) to avoid conflicts with user-defined metadata. Component versions are also included in the metadata when available (e.g., `meta.fastmcp.version`). Other MCP server implementations may not provide this metadata structure.
</Note>
## Using Prompts

View file

@ -36,7 +36,7 @@ async with client:
print(f"MIME Type: {resource.mimeType}")
# Access tags and other metadata
if resource.meta:
fastmcp_meta = resource.meta.get('_fastmcp', {})
fastmcp_meta = resource.meta.get('fastmcp', {})
print(f"Tags: {fastmcp_meta.get('tags', [])}")
```
@ -55,7 +55,7 @@ async with client:
print(f"Description: {template.description}")
# Access tags and other metadata
if template.meta:
fastmcp_meta = template.meta.get('_fastmcp', {})
fastmcp_meta = template.meta.get('fastmcp', {})
print(f"Tags: {fastmcp_meta.get('tags', [])}")
```
@ -73,15 +73,15 @@ async with client:
config_resources = [
resource for resource in resources
if resource.meta and
resource.meta.get('_fastmcp', {}) and
'config' in resource.meta.get('_fastmcp', {}).get('tags', [])
resource.meta.get('fastmcp', {}) and
'config' in resource.meta.get('fastmcp', {}).get('tags', [])
]
print(f"Found {len(config_resources)} config resources")
```
<Note>
The `meta` field is part of the standard MCP specification. FastMCP servers include tags and other metadata within a `_fastmcp` namespace (e.g., `meta._fastmcp.tags`) to avoid conflicts with user-defined metadata. This behavior can be controlled with the server's `include_fastmcp_meta` setting - when disabled, the `_fastmcp` namespace won't be included. Other MCP server implementations may not provide this metadata structure.
The `meta` field is part of the standard MCP specification. FastMCP servers always include tags and other metadata within a `fastmcp` namespace (e.g., `meta.fastmcp.tags`) to avoid conflicts with user-defined metadata. Component versions are also included in the metadata when available (e.g., `meta.fastmcp.version`). Other MCP server implementations may not provide this metadata structure.
</Note>
## Reading Resources

View file

@ -27,7 +27,7 @@ async with client:
print(f"Parameters: {tool.inputSchema}")
# Access tags and other metadata
if tool.meta:
fastmcp_meta = tool.meta.get('_fastmcp', {})
fastmcp_meta = tool.meta.get('fastmcp', {})
print(f"Tags: {fastmcp_meta.get('tags', [])}")
```
@ -45,15 +45,15 @@ async with client:
analysis_tools = [
tool for tool in tools
if tool.meta and
tool.meta.get('_fastmcp', {}) and
'analysis' in tool.meta.get('_fastmcp', {}).get('tags', [])
tool.meta.get('fastmcp', {}) and
'analysis' in tool.meta.get('fastmcp', {}).get('tags', [])
]
print(f"Found {len(analysis_tools)} analysis tools")
```
<Note>
The `meta` field is part of the standard MCP specification. FastMCP servers include tags and other metadata within a `_fastmcp` namespace (e.g., `meta._fastmcp.tags`) to avoid conflicts with user-defined metadata. This behavior can be controlled with the server's `include_fastmcp_meta` setting - when disabled, the `_fastmcp` namespace won't be included. Other MCP server implementations may not provide this metadata structure.
The `meta` field is part of the standard MCP specification. FastMCP servers always include tags and other metadata within a `fastmcp` namespace (e.g., `meta.fastmcp.tags`) to avoid conflicts with user-defined metadata. Component versions are also included in the metadata when available (e.g., `meta.fastmcp.version`). Other MCP server implementations may not provide this metadata structure.
</Note>
## Executing Tools

View file

@ -8,6 +8,68 @@ tag: NEW
This guide provides migration instructions for breaking changes and major updates when upgrading between FastMCP versions.
## FastMCP Metadata Namespace Change
### Metadata Namespace Renamed
The FastMCP metadata namespace has been renamed from `_fastmcp` to `fastmcp` (underscore prefix removed). All metadata is now always included in component responses.
**What changed:**
- Metadata namespace: `meta._fastmcp` → `meta.fastmcp`
- The `include_fastmcp_meta` setting and parameter have been removed
- Component version is now included in metadata when available (`meta.fastmcp.version`)
**Migration steps:**
1. **Update metadata access patterns:**
<CodeGroup>
```python Before
tags = tool.meta.get("_fastmcp", {}).get("tags", [])
```
```python After
tags = tool.meta.get("fastmcp", {}).get("tags", [])
```
</CodeGroup>
2. **Remove `include_fastmcp_meta` parameter:**
<CodeGroup>
```python Before
mcp = FastMCP(include_fastmcp_meta=False)
```
```python After
# Parameter removed - metadata is always included
mcp = FastMCP()
```
</CodeGroup>
3. **Remove `include_fastmcp_meta` from component serialization:**
<CodeGroup>
```python Before
mcp_tool = tool.to_mcp_tool(include_fastmcp_meta=True)
```
```python After
mcp_tool = tool.to_mcp_tool()
```
</CodeGroup>
4. **Access component version from metadata:**
<CodeGroup>
```python Before
# Version was not available in metadata
```
```python After
version = tool.meta.get("fastmcp", {}).get("version")
if version:
print(f"Tool version: {version}")
```
</CodeGroup>
**Why this changed:** The underscore prefix was removed to make the namespace more discoverable and consistent with standard naming conventions. Making metadata always included simplifies the API and ensures consistent behavior across all FastMCP servers.
## v3.0.0
### WSTransport Removed

View file

@ -324,7 +324,7 @@ mcp = FastMCP.from_openapi(
#### OpenAPI Tags in Client Meta
FastMCP automatically includes OpenAPI tags from your specification in the component's metadata. These tags are available to MCP clients through the `meta._fastmcp.tags` field, allowing clients to filter and organize components based on the original OpenAPI tagging:
FastMCP automatically includes OpenAPI tags from your specification in the component's metadata. These tags are available to MCP clients through the `meta.fastmcp.tags` field, allowing clients to filter and organize components based on the original OpenAPI tagging:
<CodeGroup>
```json {5} OpenAPI spec with tags
@ -345,8 +345,8 @@ async with client:
tools = await client.list_tools()
for tool in tools:
if tool.meta:
# OpenAPI tags are now available in _fastmcp namespace!
fastmcp_meta = tool.meta.get('_fastmcp', {})
# OpenAPI tags are now available in fastmcp namespace!
fastmcp_meta = tool.meta.get('fastmcp', {})
openapi_tags = fastmcp_meta.get('tags', [])
if 'users' in openapi_tags:
print(f"Found user-related tool: {tool.name}")

View file

@ -95,11 +95,6 @@ The `FastMCP` constructor accepts several arguments:
Controls how tool input parameters are validated. When `False` (default), FastMCP uses Pydantic's flexible validation that coerces compatible inputs (e.g., `"10"` → `10` for int parameters). When `True`, uses the MCP SDK's JSON Schema validation to validate inputs against the exact schema before passing them to your function, rejecting any type mismatches. The default mode improves compatibility with LLM clients while maintaining type safety. See [Input Validation Modes](/servers/tools#input-validation-modes) for details
</ParamField>
<ParamField body="include_fastmcp_meta" type="bool" default="True">
<VersionBadge version="2.11.0" />
Whether to include FastMCP metadata in component responses. When `True`, component tags and other FastMCP-specific metadata are included in the `_fastmcp` namespace within each component's `meta` field. When `False`, this metadata is omitted, resulting in cleaner integration with external systems. Can be overridden globally via `FASTMCP_INCLUDE_FASTMCP_META` environment variable
</ParamField>
</Card>
## Components
@ -344,7 +339,6 @@ mcp = FastMCP(
on_duplicate_tools="error", # Handle duplicate registrations
on_duplicate_resources="warn",
on_duplicate_prompts="replace",
include_fastmcp_meta=False, # Disable FastMCP metadata for cleaner integration
)
```
@ -359,14 +353,12 @@ import fastmcp
print(fastmcp.settings.log_level) # Default: "INFO"
print(fastmcp.settings.mask_error_details) # Default: False
print(fastmcp.settings.strict_input_validation) # Default: False
print(fastmcp.settings.include_fastmcp_meta) # Default: True
```
Common global settings include:
- **`log_level`**: Logging level ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"), set with `FASTMCP_LOG_LEVEL`
- **`mask_error_details`**: Whether to hide detailed error information from clients, set with `FASTMCP_MASK_ERROR_DETAILS`
- **`strict_input_validation`**: Controls tool input validation mode (default: False for flexible coercion), set with `FASTMCP_STRICT_INPUT_VALIDATION`. See [Input Validation Modes](/servers/tools#input-validation-modes)
- **`include_fastmcp_meta`**: Whether to include FastMCP metadata in component responses (default: True), set with `FASTMCP_INCLUDE_FASTMCP_META`
- **`env_file`**: Path to the environment file to load settings from (default: ".env"), set with `FASTMCP_ENV_FILE`. Useful when your project uses a `.env` file with syntax incompatible with python-dotenv
### Transport-Specific Configuration
@ -399,6 +391,5 @@ Global FastMCP settings can be configured via environment variables (prefixed wi
export FASTMCP_LOG_LEVEL=DEBUG
export FASTMCP_MASK_ERROR_DETAILS=True
export FASTMCP_STRICT_INPUT_VALIDATION=False
export FASTMCP_INCLUDE_FASTMCP_META=False
```

View file

@ -200,8 +200,6 @@ class Prompt(FastMCPComponent):
def to_mcp_prompt(
self,
*,
include_fastmcp_meta: bool | None = None,
**overrides: Any,
) -> SDKPrompt:
"""Convert the prompt to an MCP prompt."""
@ -221,7 +219,7 @@ class Prompt(FastMCPComponent):
title=overrides.get("title", self.title),
icons=overrides.get("icons", self.icons),
_meta=overrides.get( # type: ignore[call-arg] # _meta is Pydantic alias for meta field
"_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
"_meta", self.get_meta()
),
)

View file

@ -357,8 +357,6 @@ class Resource(FastMCPComponent):
def to_mcp_resource(
self,
*,
include_fastmcp_meta: bool | None = None,
**overrides: Any,
) -> SDKResource:
"""Convert the resource to an SDKResource."""
@ -372,7 +370,7 @@ class Resource(FastMCPComponent):
icons=overrides.get("icons", self.icons),
annotations=overrides.get("annotations", self.annotations),
_meta=overrides.get( # type: ignore[call-arg] # _meta is Pydantic alias for meta field
"_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
"_meta", self.get_meta()
),
)

View file

@ -251,8 +251,6 @@ class ResourceTemplate(FastMCPComponent):
def to_mcp_template(
self,
*,
include_fastmcp_meta: bool | None = None,
**overrides: Any,
) -> SDKResourceTemplate:
"""Convert the resource template to an SDKResourceTemplate."""
@ -266,7 +264,7 @@ class ResourceTemplate(FastMCPComponent):
icons=overrides.get("icons", self.icons),
annotations=overrides.get("annotations", self.annotations),
_meta=overrides.get( # type: ignore[call-arg] # _meta is Pydantic alias for meta field
"_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
"_meta", self.get_meta()
),
)

View file

@ -44,7 +44,7 @@ from fastmcp.server.providers.base import Provider
from fastmcp.server.server import FastMCP
from fastmcp.server.tasks.config import TaskConfig
from fastmcp.tools.tool import Tool, ToolResult
from fastmcp.utilities.components import FastMCPComponent
from fastmcp.utilities.components import FastMCPComponent, get_fastmcp_metadata
from fastmcp.utilities.logging import get_logger
if TYPE_CHECKING:
@ -104,7 +104,7 @@ class ProxyTool(Tool):
output_schema=mcp_tool.outputSchema,
icons=mcp_tool.icons,
meta=mcp_tool.meta,
tags=(mcp_tool.meta or {}).get("_fastmcp", {}).get("tags", []),
tags=get_fastmcp_metadata(mcp_tool.meta).get("tags", []),
)
async def run(
@ -211,7 +211,7 @@ class ProxyResource(Resource):
mime_type=mcp_resource.mimeType or "text/plain",
icons=mcp_resource.icons,
meta=mcp_resource.meta,
tags=(mcp_resource.meta or {}).get("_fastmcp", {}).get("tags", []),
tags=get_fastmcp_metadata(mcp_resource.meta).get("tags", []),
task_config=TaskConfig(mode="forbidden"),
)
@ -309,7 +309,7 @@ class ProxyTemplate(ResourceTemplate):
icons=mcp_template.icons,
parameters={}, # Remote templates don't have local parameters
meta=mcp_template.meta,
tags=(mcp_template.meta or {}).get("_fastmcp", {}).get("tags", []),
tags=get_fastmcp_metadata(mcp_template.meta).get("tags", []),
task_config=TaskConfig(mode="forbidden"),
)
@ -371,7 +371,7 @@ class ProxyTemplate(ResourceTemplate):
].mimeType, # Use first item's mimeType for backward compatibility
icons=self.icons,
meta=self.meta,
tags=(self.meta or {}).get("_fastmcp", {}).get("tags", []),
tags=get_fastmcp_metadata(self.meta).get("tags", []),
_cached_content=cached_content,
)
@ -429,7 +429,7 @@ class ProxyPrompt(Prompt):
arguments=arguments,
icons=mcp_prompt.icons,
meta=mcp_prompt.meta,
tags=(mcp_prompt.meta or {}).get("_fastmcp", {}).get("tags", []),
tags=get_fastmcp_metadata(mcp_prompt.meta).get("tags", []),
task_config=TaskConfig(mode="forbidden"),
)

View file

@ -248,7 +248,6 @@ class FastMCP(Generic[LifespanResultT]):
tool_serializer: ToolResultSerializerType | None = None,
include_tags: Collection[str] | None = None,
exclude_tags: Collection[str] | None = None,
include_fastmcp_meta: bool | None = None,
on_duplicate: DuplicateBehavior | None = None,
strict_input_validation: bool | None = None,
tasks: bool | None = None,
@ -406,12 +405,6 @@ class FastMCP(Generic[LifespanResultT]):
sampling_handler_behavior or "fallback"
)
self.include_fastmcp_meta: bool = (
include_fastmcp_meta
if include_fastmcp_meta is not None
else fastmcp.settings.include_fastmcp_meta
)
self._handle_deprecated_settings(
log_level=log_level,
debug=debug,
@ -1939,7 +1932,6 @@ class FastMCP(Generic[LifespanResultT]):
return [
tool.to_mcp_tool(
name=tool.name,
include_fastmcp_meta=self.include_fastmcp_meta,
)
for tool in tools
]
@ -1956,7 +1948,6 @@ class FastMCP(Generic[LifespanResultT]):
return [
resource.to_mcp_resource(
uri=str(resource.uri),
include_fastmcp_meta=self.include_fastmcp_meta,
)
for resource in resources
]
@ -1973,7 +1964,6 @@ class FastMCP(Generic[LifespanResultT]):
return [
template.to_mcp_template(
uriTemplate=template.uri_template,
include_fastmcp_meta=self.include_fastmcp_meta,
)
for template in templates
]
@ -1990,7 +1980,6 @@ class FastMCP(Generic[LifespanResultT]):
return [
prompt.to_mcp_prompt(
name=prompt.name,
include_fastmcp_meta=self.include_fastmcp_meta,
)
for prompt in prompts
]

View file

@ -305,20 +305,6 @@ class Settings(BaseSettings):
False # If True, uses true stateless mode (new transport per request)
)
include_fastmcp_meta: Annotated[
bool,
Field(
description=inspect.cleandoc(
"""
Whether to include FastMCP meta in the server's MCP responses.
If True, a `_fastmcp` key will be added to the `meta` field of
all MCP component responses. This key will contain a dict of
various FastMCP-specific metadata, such as tags.
"""
),
),
] = True
mounted_components_raise_on_load_error: Annotated[
bool,
Field(

View file

@ -84,8 +84,6 @@ class FunctionTool(Tool):
def to_mcp_tool(
self,
*,
include_fastmcp_meta: bool | None = None,
**overrides: Any,
) -> mcp.types.Tool:
"""Convert the FastMCP tool to an MCP tool.
@ -93,9 +91,7 @@ class FunctionTool(Tool):
Extends the base implementation to add task execution mode if enabled.
"""
# Get base MCP tool from parent
mcp_tool = super().to_mcp_tool(
include_fastmcp_meta=include_fastmcp_meta, **overrides
)
mcp_tool = super().to_mcp_tool(**overrides)
# Add task execution mode per SEP-1686
# Only set execution if not overridden and task execution is supported

View file

@ -164,8 +164,6 @@ class Tool(FastMCPComponent):
def to_mcp_tool(
self,
*,
include_fastmcp_meta: bool | None = None,
**overrides: Any,
) -> MCPTool:
"""Convert the FastMCP tool to an MCP tool."""
@ -186,7 +184,7 @@ class Tool(FastMCPComponent):
annotations=overrides.get("annotations", self.annotations),
execution=overrides.get("execution", self.execution),
_meta=overrides.get( # type: ignore[call-arg] # _meta is Pydantic alias for meta field
"_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
"_meta", self.get_meta()
),
)

View file

@ -1,13 +1,12 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import TYPE_CHECKING, Annotated, Any, ClassVar, TypedDict
from typing import TYPE_CHECKING, Annotated, Any, ClassVar, TypedDict, cast
from mcp.types import Icon
from pydantic import BeforeValidator, Field
from typing_extensions import Self, TypeVar
import fastmcp
from fastmcp.server.tasks.config import TaskConfig
from fastmcp.utilities.types import FastMCPBaseModel
@ -20,6 +19,18 @@ T = TypeVar("T", default=Any)
class FastMCPMeta(TypedDict, total=False):
tags: list[str]
version: str
def get_fastmcp_metadata(meta: dict[str, Any] | None) -> FastMCPMeta:
"""Extract FastMCP metadata from a component's meta dict.
Handles both the current `fastmcp` namespace and the legacy `_fastmcp`
namespace for compatibility with older FastMCP servers.
"""
if not meta:
return {}
return cast(FastMCPMeta, meta.get("fastmcp") or meta.get("_fastmcp") or {})
def _convert_set_default_none(maybe_set: set[T] | Sequence[T] | None) -> set[T]:
@ -126,29 +137,27 @@ class FastMCPComponent(FastMCPBaseModel):
base_key = self.make_key(self.name)
return f"{base_key}@{self.version or ''}"
def get_meta(
self, include_fastmcp_meta: bool | None = None
) -> dict[str, Any] | None:
def get_meta(self) -> dict[str, Any]:
"""Get the meta information about the component.
Returns a dict that always includes a `fastmcp` key containing:
- `tags`: sorted list of component tags
- `version`: component version (only if set)
"""
Get the meta information about the component.
If include_fastmcp_meta is True, a `_fastmcp` key will be added to the
meta, containing a `tags` field with the tags of the component.
"""
if include_fastmcp_meta is None:
include_fastmcp_meta = fastmcp.settings.include_fastmcp_meta
meta = self.meta or {}
if include_fastmcp_meta:
fastmcp_meta = FastMCPMeta(tags=sorted(self.tags))
# overwrite any existing _fastmcp meta with keys from the new one
if upstream_meta := meta.get("_fastmcp"):
fastmcp_meta = upstream_meta | fastmcp_meta
meta["_fastmcp"] = fastmcp_meta
fastmcp_meta: FastMCPMeta = {"tags": sorted(self.tags)}
if self.version is not None:
fastmcp_meta["version"] = self.version
return meta or None
# overwrite any existing fastmcp meta with keys from the new one
if (upstream_meta := meta.get("fastmcp")) is not None:
if not isinstance(upstream_meta, dict):
raise TypeError("meta['fastmcp'] must be a dict")
fastmcp_meta = upstream_meta | fastmcp_meta
meta["fastmcp"] = fastmcp_meta
return meta
def __eq__(self, other: object) -> bool:
if type(self) is not type(other):

View file

@ -333,7 +333,7 @@ class TestPromptToolMiddleware:
[
TextContent(
type="text",
text='[{"name":"greeting","title":null,"description":"Generate a greeting message.","arguments":[{"name":"name","description":null,"required":true}],"icons":null,"_meta":{"_fastmcp":{"tags":[]}}},{"name":"farewell","title":null,"description":"Generate a farewell message.","arguments":[{"name":"name","description":null,"required":true}],"icons":null,"_meta":{"_fastmcp":{"tags":[]}}}]',
text='[{"name":"greeting","title":null,"description":"Generate a greeting message.","arguments":[{"name":"name","description":null,"required":true}],"icons":null,"_meta":{"fastmcp":{"tags":[]}}},{"name":"farewell","title":null,"description":"Generate a farewell message.","arguments":[{"name":"name","description":null,"required":true}],"icons":null,"_meta":{"fastmcp":{"tags":[]}}}]',
)
]
)
@ -348,7 +348,7 @@ class TestPromptToolMiddleware:
{"name": "name", "description": None, "required": True}
],
"icons": None,
"_meta": {"_fastmcp": {"tags": []}},
"_meta": {"fastmcp": {"tags": []}},
},
{
"name": "farewell",
@ -358,7 +358,7 @@ class TestPromptToolMiddleware:
{"name": "name", "description": None, "required": True}
],
"icons": None,
"_meta": {"_fastmcp": {"tags": []}},
"_meta": {"fastmcp": {"tags": []}},
},
]
)
@ -465,7 +465,7 @@ class TestResourceToolMiddleware:
"size": None,
"icons": None,
"annotations": None,
"_meta": {"_fastmcp": {"tags": []}},
"_meta": {"fastmcp": {"tags": []}},
},
{
"name": "data_resource",
@ -476,7 +476,7 @@ class TestResourceToolMiddleware:
"size": None,
"icons": None,
"annotations": None,
"_meta": {"_fastmcp": {"tags": []}},
"_meta": {"fastmcp": {"tags": []}},
},
]
)

View file

@ -99,7 +99,7 @@ class TestProxyClient:
async with Client(proxy_server) as client:
tools = await client.list_tools()
echo_tool = next(t for t in tools if t.name == "echo")
assert echo_tool.meta == {"_fastmcp": {"tags": ["echo"]}}
assert echo_tool.meta == {"fastmcp": {"tags": ["echo"]}}
async def test_forward_error_response(self, proxy_server: FastMCP):
"""

View file

@ -234,7 +234,7 @@ class TestTools:
tools = await proxy_server.get_tools()
greet_tool = next(t for t in tools if t.name == "greet")
assert greet_tool.title == "Greet"
assert greet_tool.meta == {"_fastmcp": {"tags": ["greet"]}}
assert greet_tool.meta == {"fastmcp": {"tags": ["greet"]}}
assert greet_tool.icons == [Icon(src="https://example.com/greet-icon.png")]
async def test_get_transformed_tools(self):
@ -367,7 +367,7 @@ class TestResources:
resources = await proxy_server.get_resources()
wave_resource = next(r for r in resources if str(r.uri) == "resource://wave")
assert wave_resource.title == "Wave"
assert wave_resource.meta == {"_fastmcp": {"tags": ["wave"]}}
assert wave_resource.meta == {"fastmcp": {"tags": ["wave"]}}
assert wave_resource.icons == [Icon(src="https://example.com/wave-icon.png")]
async def test_list_resources_same_as_original(self, fastmcp_server, proxy_server):
@ -481,7 +481,7 @@ class TestResourceTemplates:
t for t in templates if t.uri_template == "data://user/{user_id}"
)
assert get_user_template.title == "User Template"
assert get_user_template.meta == {"_fastmcp": {"tags": ["users"]}}
assert get_user_template.meta == {"fastmcp": {"tags": ["users"]}}
assert get_user_template.icons == [
Icon(src="https://example.com/user-icon.png")
]
@ -589,7 +589,7 @@ class TestPrompts:
prompts = await proxy_server.get_prompts()
welcome_prompt = next(p for p in prompts if p.name == "welcome")
assert welcome_prompt.title == "Welcome"
assert welcome_prompt.meta == {"_fastmcp": {"tags": ["welcome"]}}
assert welcome_prompt.meta == {"fastmcp": {"tags": ["welcome"]}}
assert welcome_prompt.icons == [
Icon(src="https://example.com/welcome-icon.png")
]

View file

@ -235,10 +235,10 @@ class TestAbstractCollectionTypes:
class TestMeta:
"""Test that include_fastmcp_meta controls whether _fastmcp key is present in meta."""
"""Test that fastmcp key is always present in meta."""
async def test_tool_tags_in_meta_with_default_setting(self):
"""Test that tool tags appear in meta under _fastmcp key with default setting."""
async def test_tool_tags_in_meta(self):
"""Test that tool tags appear in meta under fastmcp key."""
mcp = FastMCP()
@mcp.tool(tags={"tool-example", "test-tool-tag"})
@ -250,13 +250,13 @@ class TestMeta:
tools = await client.list_tools()
tool = next(t for t in tools if t.name == "sample_tool")
assert tool.meta is not None
assert set(tool.meta["_fastmcp"]["tags"]) == {
assert set(tool.meta["fastmcp"]["tags"]) == {
"tool-example",
"test-tool-tag",
}
async def test_resource_tags_in_meta_with_default_setting(self):
"""Test that resource tags appear in meta under _fastmcp key with default setting."""
async def test_resource_tags_in_meta(self):
"""Test that resource tags appear in meta under fastmcp key."""
mcp = FastMCP()
@mcp.resource(
@ -270,13 +270,13 @@ class TestMeta:
resources = await client.list_resources()
resource = next(r for r in resources if str(r.uri) == "test://resource")
assert resource.meta is not None
assert set(resource.meta["_fastmcp"]["tags"]) == {
assert set(resource.meta["fastmcp"]["tags"]) == {
"resource-example",
"test-resource-tag",
}
async def test_resource_template_tags_in_meta_with_default_setting(self):
"""Test that resource template tags appear in meta under _fastmcp key with default setting."""
async def test_resource_template_tags_in_meta(self):
"""Test that resource template tags appear in meta under fastmcp key."""
mcp = FastMCP()
@mcp.resource(
@ -292,13 +292,13 @@ class TestMeta:
t for t in templates if t.uriTemplate == "test://template/{id}"
)
assert template.meta is not None
assert set(template.meta["_fastmcp"]["tags"]) == {
assert set(template.meta["fastmcp"]["tags"]) == {
"template-example",
"test-template-tag",
}
async def test_prompt_tags_in_meta_with_default_setting(self):
"""Test that prompt tags appear in meta under _fastmcp key with default setting."""
async def test_prompt_tags_in_meta(self):
"""Test that prompt tags appear in meta under fastmcp key."""
mcp = FastMCP()
@mcp.prompt(tags={"example", "test-tag"})
@ -309,105 +309,7 @@ class TestMeta:
prompts = await client.list_prompts()
prompt = next(p for p in prompts if p.name == "sample_prompt")
assert prompt.meta is not None
assert set(prompt.meta["_fastmcp"]["tags"]) == {"example", "test-tag"}
async def test_tool_meta_with_include_fastmcp_meta_false(self):
mcp = FastMCP(include_fastmcp_meta=False)
@mcp.tool(tags={"tool-example", "test-tool-tag"})
def sample_tool(x: int) -> int:
"""A sample tool."""
return x * 2
async with Client(mcp) as client:
tools = await client.list_tools()
tool = next(t for t in tools if t.name == "sample_tool")
# Meta should be None when include_fastmcp_meta is False
assert tool.meta is None
async def test_resource_meta_with_include_fastmcp_meta_false(self):
mcp = FastMCP(include_fastmcp_meta=False)
@mcp.resource(
uri="test://resource", tags={"resource-example", "test-resource-tag"}
)
def sample_resource() -> str:
"""A sample resource."""
return "resource content"
async with Client(mcp) as client:
resources = await client.list_resources()
resource = next(r for r in resources if str(r.uri) == "test://resource")
# Meta should be None when include_fastmcp_meta is False
assert resource.meta is None
async def test_resource_template_meta_with_include_fastmcp_meta_false(self):
mcp = FastMCP(include_fastmcp_meta=False)
@mcp.resource(
"test://template/{id}", tags={"template-example", "test-template-tag"}
)
def sample_template(id: str) -> str:
"""A sample resource template."""
return f"template content for {id}"
async with Client(mcp) as client:
templates = await client.list_resource_templates()
template = next(
t for t in templates if t.uriTemplate == "test://template/{id}"
)
# Meta should be None when include_fastmcp_meta is False
assert template.meta is None
async def test_prompt_meta_with_include_fastmcp_meta_false(self):
mcp = FastMCP(include_fastmcp_meta=False)
@mcp.prompt(tags={"example", "test-tag"})
def sample_prompt() -> str:
return "Hello, world!"
async with Client(mcp) as client:
prompts = await client.list_prompts()
prompt = next(p for p in prompts if p.name == "sample_prompt")
# Meta should be None when include_fastmcp_meta is False
assert prompt.meta is None
async def test_temporary_include_fastmcp_meta_setting(self):
"""Test that temporary_settings can toggle include_fastmcp_meta for new servers."""
def make_server() -> FastMCP:
mcp = FastMCP()
@mcp.tool(tags={"test-tag"})
def sample_tool(x: int) -> int:
"""A sample tool."""
return x * 2
return mcp
# Default: meta should be present
mcp = make_server()
async with Client(mcp) as client:
tools = await client.list_tools()
tool = next(t for t in tools if t.name == "sample_tool")
assert tool.meta is not None
assert set(tool.meta["_fastmcp"]["tags"]) == {"test-tag"}
# With setting disabled: new server should not include meta
with temporary_settings(include_fastmcp_meta=False):
mcp = make_server()
async with Client(mcp) as client:
tools = await client.list_tools()
tool = next(t for t in tools if t.name == "sample_tool")
assert tool.meta is None
# After context: new server should have meta again
mcp = make_server()
async with Client(mcp) as client:
tools = await client.list_tools()
tool = next(t for t in tools if t.name == "sample_tool")
assert tool.meta is not None
assert set(tool.meta["_fastmcp"]["tags"]) == {"test-tag"}
assert set(prompt.meta["fastmcp"]["tags"]) == {"example", "test-tag"}
class TestShowServerBannerSetting:

View file

@ -86,39 +86,50 @@ class TestFastMCPComponent:
# Base component has no KEY_PREFIX, so key is just "name@version" (or "name@" for unversioned)
assert basic_component.key == "test_component@"
def test_get_meta_without_fastmcp_meta(self, basic_component):
"""Test get_meta without including fastmcp meta."""
basic_component.meta = {"custom": "data"}
result = basic_component.get_meta(include_fastmcp_meta=False)
assert result == {"custom": "data"}
assert "_fastmcp" not in result
def test_get_meta_with_fastmcp_meta(self, basic_component):
"""Test get_meta including fastmcp meta."""
"""Test get_meta always includes fastmcp meta."""
basic_component.meta = {"custom": "data"}
basic_component.tags = {"tag2", "tag1"} # Unordered to test sorting
result = basic_component.get_meta(include_fastmcp_meta=True)
result = basic_component.get_meta()
assert result["custom"] == "data"
assert "_fastmcp" in result
assert result["_fastmcp"]["tags"] == ["tag1", "tag2"] # Should be sorted
assert "fastmcp" in result
assert result["fastmcp"]["tags"] == ["tag1", "tag2"] # Should be sorted
def test_get_meta_preserves_existing_fastmcp_meta(self):
"""Test that get_meta preserves existing _fastmcp meta."""
"""Test that get_meta preserves existing fastmcp meta."""
component = FastMCPComponent(
name="test",
meta={"_fastmcp": {"existing": "value"}},
meta={"fastmcp": {"existing": "value"}},
tags={"new_tag"},
)
result = component.get_meta(include_fastmcp_meta=True)
result = component.get_meta()
assert result is not None
assert result["_fastmcp"]["existing"] == "value"
assert result["_fastmcp"]["tags"] == ["new_tag"]
assert result["fastmcp"]["existing"] == "value"
assert result["fastmcp"]["tags"] == ["new_tag"]
def test_get_meta_returns_none_when_empty(self):
"""Test that get_meta returns None when no meta and fastmcp_meta is False."""
def test_get_meta_returns_dict_with_fastmcp_when_empty(self):
"""Test that get_meta returns dict with fastmcp meta even when no custom meta."""
component = FastMCPComponent(name="test")
result = component.get_meta(include_fastmcp_meta=False)
assert result is None
result = component.get_meta()
assert result is not None
assert "fastmcp" in result
assert result["fastmcp"]["tags"] == []
def test_get_meta_includes_version(self):
"""Test that get_meta includes version when component has a version."""
component = FastMCPComponent(name="test", version="v1.0.0", tags={"tag1"})
result = component.get_meta()
assert result is not None
assert result["fastmcp"]["version"] == "v1.0.0"
assert result["fastmcp"]["tags"] == ["tag1"]
def test_get_meta_excludes_version_when_none(self):
"""Test that get_meta excludes version when component has no version."""
component = FastMCPComponent(name="test", tags={"tag1"})
result = component.get_meta()
assert result is not None
assert "version" not in result["fastmcp"]
assert result["fastmcp"]["tags"] == ["tag1"]
def test_equality_same_components(self):
"""Test that identical components are equal."""
@ -289,10 +300,17 @@ class TestFastMCPMeta:
meta: FastMCPMeta = {"tags": ["tag1", "tag2"]}
assert meta["tags"] == ["tag1", "tag2"]
def test_fastmcp_meta_with_version(self):
"""Test that FastMCPMeta can include version."""
meta: FastMCPMeta = {"tags": ["tag1"], "version": "v1.0.0"}
assert meta["tags"] == ["tag1"]
assert meta["version"] == "v1.0.0"
def test_fastmcp_meta_optional_fields(self):
"""Test that FastMCPMeta fields are optional."""
meta: FastMCPMeta = {}
assert "tags" not in meta # Should be optional
assert "version" not in meta # Should be optional
class TestEdgeCasesAndIntegration:
@ -312,7 +330,7 @@ class TestEdgeCasesAndIntegration:
def test_meta_mutation_affects_original(self):
"""Test that get_meta returns a reference to the original meta."""
component = FastMCPComponent(name="test", meta={"key": "value"})
meta = component.get_meta(include_fastmcp_meta=False)
meta = component.get_meta()
assert meta is not None
meta["key"] = "modified"
assert component.meta is not None