diff --git a/docs/clients/prompts.mdx b/docs/clients/prompts.mdx index 1c05b75b3f..c4ae2f4d4d 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 22ebafab81..cef312267a 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 4d7ebabc4e..17bc9f6924 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 c5465f1b3c..cb59818a93 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 7eceea4077..88dc19b142 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 90cbcd7e9e..777c7adf16 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 fde2c9e4f0..57bbecd56a 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 @@ def to_mcp_prompt( 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 5dd907348c..6b23468e4d 100644 --- a/src/fastmcp/resources/resource.py +++ b/src/fastmcp/resources/resource.py @@ -357,8 +357,6 @@ async def _read( def to_mcp_resource( self, - *, - include_fastmcp_meta: bool | None = None, **overrides: Any, ) -> SDKResource: """Convert the resource to an SDKResource.""" @@ -372,7 +370,7 @@ def to_mcp_resource( 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 63c77e2c3b..343c124240 100644 --- a/src/fastmcp/resources/template.py +++ b/src/fastmcp/resources/template.py @@ -251,8 +251,6 @@ async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource: 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 @@ def to_mcp_template( 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 699b18308a..56827ced21 100644 --- a/src/fastmcp/server/providers/proxy.py +++ b/src/fastmcp/server/providers/proxy.py @@ -44,7 +44,7 @@ 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 @@ def from_mcp_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 @@ def from_mcp_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 @@ def from_mcp_template( # type: ignore[override] 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 @@ async def create_resource( ].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 @@ def from_mcp_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 f83e8224b1..3ca76f4591 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -248,7 +248,6 @@ def __init__( 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 @@ def __init__( 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 @@ async def _list_tools_mcp(self) -> list[SDKTool]: return [ tool.to_mcp_tool( name=tool.name, - include_fastmcp_meta=self.include_fastmcp_meta, ) for tool in tools ] @@ -1956,7 +1948,6 @@ async def _list_resources_mcp(self) -> list[SDKResource]: return [ resource.to_mcp_resource( uri=str(resource.uri), - include_fastmcp_meta=self.include_fastmcp_meta, ) for resource in resources ] @@ -1973,7 +1964,6 @@ async def _list_resource_templates_mcp(self) -> list[SDKResourceTemplate]: return [ template.to_mcp_template( uriTemplate=template.uri_template, - include_fastmcp_meta=self.include_fastmcp_meta, ) for template in templates ] @@ -1990,7 +1980,6 @@ async def _list_prompts_mcp(self) -> list[SDKPrompt]: 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 5a8ca57936..0257e8d3bd 100644 --- a/src/fastmcp/settings.py +++ b/src/fastmcp/settings.py @@ -305,20 +305,6 @@ def normalize_log_level(cls, v): 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 890841ce80..be5e16f7ab 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 @@ def to_mcp_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 f4b83247ce..36b4913433 100644 --- a/src/fastmcp/tools/tool.py +++ b/src/fastmcp/tools/tool.py @@ -164,8 +164,6 @@ def _validate_tool_name(self) -> Tool: 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 @@ def to_mcp_tool( 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 b9a7f2eab9..53bcc17d6f 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 @@ 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 @@ def key(self) -> str: 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: - """ - Get the meta information about the component. + def get_meta(self) -> dict[str, Any]: + """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. + Returns a dict that always includes a `fastmcp` key containing: + - `tags`: sorted list of component tags + - `version`: component version (only if set) """ - - 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 + + # 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 or None + 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 863c575040..5a583f5435 100644 --- a/tests/server/middleware/test_tool_injection.py +++ b/tests/server/middleware/test_tool_injection.py @@ -333,7 +333,7 @@ async def test_list_prompts_tool_works(self, server_with_prompts: FastMCP): [ 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 @@ async def test_list_prompts_tool_works(self, server_with_prompts: FastMCP): {"name": "name", "description": None, "required": True} ], "icons": None, - "_meta": {"_fastmcp": {"tags": []}}, + "_meta": {"fastmcp": {"tags": []}}, }, { "name": "farewell", @@ -358,7 +358,7 @@ async def test_list_prompts_tool_works(self, server_with_prompts: FastMCP): {"name": "name", "description": None, "required": True} ], "icons": None, - "_meta": {"_fastmcp": {"tags": []}}, + "_meta": {"fastmcp": {"tags": []}}, }, ] ) @@ -465,7 +465,7 @@ async def test_list_resources_tool_works(self, server_with_resources: FastMCP): "size": None, "icons": None, "annotations": None, - "_meta": {"_fastmcp": {"tags": []}}, + "_meta": {"fastmcp": {"tags": []}}, }, { "name": "data_resource", @@ -476,7 +476,7 @@ async def test_list_resources_tool_works(self, server_with_resources: FastMCP): "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 8582a2257d..7b3bb19b96 100644 --- a/tests/server/providers/proxy/test_proxy_client.py +++ b/tests/server/providers/proxy/test_proxy_client.py @@ -99,7 +99,7 @@ async def test_forward_tool_meta(self, proxy_server: FastMCP): 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 8c0ec2b7d1..ecad2062d3 100644 --- a/tests/server/providers/proxy/test_proxy_server.py +++ b/tests/server/providers/proxy/test_proxy_server.py @@ -234,7 +234,7 @@ async def test_get_tools_meta(self, proxy_server): 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 @@ async def test_get_resources_meta(self, proxy_server): 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 @@ async def test_get_resource_templates_meta(self, proxy_server): 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 @@ async def test_get_prompts_meta(self, proxy_server): 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 e65f55d80e..a87c69a300 100644 --- a/tests/server/test_server.py +++ b/tests/server/test_server.py @@ -235,10 +235,10 @@ def greet(name: str) -> str: 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 @@ def sample_tool(x: int) -> int: 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 @@ def sample_resource() -> str: 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 @@ def sample_template(id: str) -> str: 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 @@ def sample_prompt() -> str: 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 fa01a7dd62..b64ee37d61 100644 --- a/tests/utilities/test_components.py +++ b/tests/utilities/test_components.py @@ -86,39 +86,50 @@ def test_key_property_without_custom_key(self, basic_component): # 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 @@ def test_fastmcp_meta_structure(self): 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 @@ def test_tags_with_none_values(self): 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