diff --git a/docs/clients/prompts.mdx b/docs/clients/prompts.mdx
index 1c05b75b3..c4ae2f4d4 100644
--- a/docs/clients/prompts.mdx
+++ b/docs/clients/prompts.mdx
@@ -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")
```
-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.
## Using Prompts
diff --git a/docs/clients/resources.mdx b/docs/clients/resources.mdx
index 22ebafab8..cef312267 100644
--- a/docs/clients/resources.mdx
+++ b/docs/clients/resources.mdx
@@ -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")
```
-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.
## Reading Resources
diff --git a/docs/clients/tools.mdx b/docs/clients/tools.mdx
index 4d7ebabc4..17bc9f692 100644
--- a/docs/clients/tools.mdx
+++ b/docs/clients/tools.mdx
@@ -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")
```
-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.
## Executing Tools
diff --git a/docs/development/upgrade-guide.mdx b/docs/development/upgrade-guide.mdx
index c5465f1b3..cb59818a9 100644
--- a/docs/development/upgrade-guide.mdx
+++ b/docs/development/upgrade-guide.mdx
@@ -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:**
+
+ ```python Before
+ tags = tool.meta.get("_fastmcp", {}).get("tags", [])
+ ```
+
+ ```python After
+ tags = tool.meta.get("fastmcp", {}).get("tags", [])
+ ```
+
+
+2. **Remove `include_fastmcp_meta` parameter:**
+
+ ```python Before
+ mcp = FastMCP(include_fastmcp_meta=False)
+ ```
+
+ ```python After
+ # Parameter removed - metadata is always included
+ mcp = FastMCP()
+ ```
+
+
+3. **Remove `include_fastmcp_meta` from component serialization:**
+
+ ```python Before
+ mcp_tool = tool.to_mcp_tool(include_fastmcp_meta=True)
+ ```
+
+ ```python After
+ mcp_tool = tool.to_mcp_tool()
+ ```
+
+
+4. **Access component version from metadata:**
+
+ ```python Before
+ # Version was not available in metadata
+ ```
+
+ ```python After
+ version = tool.meta.get("fastmcp", {}).get("version")
+ if version:
+ print(f"Tool version: {version}")
+ ```
+
+
+**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
diff --git a/docs/integrations/openapi.mdx b/docs/integrations/openapi.mdx
index 7eceea407..88dc19b14 100644
--- a/docs/integrations/openapi.mdx
+++ b/docs/integrations/openapi.mdx
@@ -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:
```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}")
diff --git a/docs/servers/server.mdx b/docs/servers/server.mdx
index 90cbcd7e9..777c7adf1 100644
--- a/docs/servers/server.mdx
+++ b/docs/servers/server.mdx
@@ -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
-
-
-
- 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
-
## 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
```
diff --git a/src/fastmcp/prompts/prompt.py b/src/fastmcp/prompts/prompt.py
index fde2c9e4f..57bbecd56 100644
--- a/src/fastmcp/prompts/prompt.py
+++ b/src/fastmcp/prompts/prompt.py
@@ -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()
),
)
diff --git a/src/fastmcp/resources/resource.py b/src/fastmcp/resources/resource.py
index 5dd907348..6b23468e4 100644
--- a/src/fastmcp/resources/resource.py
+++ b/src/fastmcp/resources/resource.py
@@ -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()
),
)
diff --git a/src/fastmcp/resources/template.py b/src/fastmcp/resources/template.py
index 63c77e2c3..343c12424 100644
--- a/src/fastmcp/resources/template.py
+++ b/src/fastmcp/resources/template.py
@@ -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()
),
)
diff --git a/src/fastmcp/server/providers/proxy.py b/src/fastmcp/server/providers/proxy.py
index 699b18308..56827ced2 100644
--- a/src/fastmcp/server/providers/proxy.py
+++ b/src/fastmcp/server/providers/proxy.py
@@ -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"),
)
diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py
index f83e8224b..3ca76f459 100644
--- a/src/fastmcp/server/server.py
+++ b/src/fastmcp/server/server.py
@@ -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
]
diff --git a/src/fastmcp/settings.py b/src/fastmcp/settings.py
index 5a8ca5793..0257e8d3b 100644
--- a/src/fastmcp/settings.py
+++ b/src/fastmcp/settings.py
@@ -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(
diff --git a/src/fastmcp/tools/function_tool.py b/src/fastmcp/tools/function_tool.py
index 890841ce8..be5e16f7a 100644
--- a/src/fastmcp/tools/function_tool.py
+++ b/src/fastmcp/tools/function_tool.py
@@ -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
diff --git a/src/fastmcp/tools/tool.py b/src/fastmcp/tools/tool.py
index f4b83247c..36b491343 100644
--- a/src/fastmcp/tools/tool.py
+++ b/src/fastmcp/tools/tool.py
@@ -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()
),
)
diff --git a/src/fastmcp/utilities/components.py b/src/fastmcp/utilities/components.py
index b9a7f2eab..53bcc17d6 100644
--- a/src/fastmcp/utilities/components.py
+++ b/src/fastmcp/utilities/components.py
@@ -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):
diff --git a/tests/server/middleware/test_tool_injection.py b/tests/server/middleware/test_tool_injection.py
index 863c57504..5a583f543 100644
--- a/tests/server/middleware/test_tool_injection.py
+++ b/tests/server/middleware/test_tool_injection.py
@@ -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": []}},
},
]
)
diff --git a/tests/server/providers/proxy/test_proxy_client.py b/tests/server/providers/proxy/test_proxy_client.py
index 8582a2257..7b3bb19b9 100644
--- a/tests/server/providers/proxy/test_proxy_client.py
+++ b/tests/server/providers/proxy/test_proxy_client.py
@@ -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):
"""
diff --git a/tests/server/providers/proxy/test_proxy_server.py b/tests/server/providers/proxy/test_proxy_server.py
index 8c0ec2b7d..ecad2062d 100644
--- a/tests/server/providers/proxy/test_proxy_server.py
+++ b/tests/server/providers/proxy/test_proxy_server.py
@@ -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")
]
diff --git a/tests/server/test_server.py b/tests/server/test_server.py
index e65f55d80..a87c69a30 100644
--- a/tests/server/test_server.py
+++ b/tests/server/test_server.py
@@ -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:
diff --git a/tests/utilities/test_components.py b/tests/utilities/test_components.py
index fa01a7dd6..b64ee37d6 100644
--- a/tests/utilities/test_components.py
+++ b/tests/utilities/test_components.py
@@ -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