Skip to content

fix: propagate version to components in FileSystemProvider#3458

Open
martimfasantos wants to merge 1 commit intoPrefectHQ:mainfrom
martimfasantos:fix/tool-version-providers
Open

fix: propagate version to components in FileSystemProvider#3458
martimfasantos wants to merge 1 commit intoPrefectHQ:mainfrom
martimfasantos:fix/tool-version-providers

Conversation

@martimfasantos
Copy link
Contributor

Summary

FileSystemProvider.extract_components() in filesystem_discovery.py does not pass version=meta.version when constructing Tool, Resource, ResourceTemplate, or Prompt objects from decorated functions. This causes all components discovered through FileSystemProvider to have version=None, breaking version-based deduplication (_dedupe_with_versions) and the meta.fastmcp.version/meta.fastmcp.versions metadata in the MCP wire output.

Problem

When tools are registered using the @tool decorator with a version parameter and discovered via FileSystemProvider, the version information is silently discarded.

Root Cause

In fastmcp/server/providers/filesystem_discovery.py, the extract_components() function reads ToolMeta, ResourceMeta, and PromptMeta from decorated functions but omits the version field when calling *.from_function():

# Current code (lines ~224-238) — version is missing for ALL component types
if isinstance(meta, ToolMeta):
    tool = Tool.from_function(
        obj,
        name=meta.name,
        title=meta.title,
        description=meta.description,
        icons=meta.icons,
        tags=meta.tags,
        output_schema=meta.output_schema,
        annotations=meta.annotations,
        meta=meta.meta,
        task=resolved_task,
        exclude_args=meta.exclude_args,
        serializer=meta.serializer,
        auth=meta.auth,
        # ❌ version=meta.version is MISSING
    )

The same omission applies to the ResourceMeta → Resource/ResourceTemplate and PromptMeta → Prompt branches.

Comparison with LocalProvider

The LocalProvider decorators correctly pass version:

# local_provider/decorators/tools.py — correct
tool = Tool.from_function(
    tool,
    ...,
    version=fmeta.version,  # ✅ version is passed
    ...
)

Impact

  1. _meta.fastmcp.version is always missing — Clients relying on tool.meta["fastmcp"]["version"] get None
  2. _meta.fastmcp.versions is never populated — The _dedupe_with_versions() function in mcp_operations.py checks c.version is not None to decide whether to inject the versions list. Since all components have version=None, the list is never built.
  3. Multi-version tools collapse silently — Two versions of the same tool (e.g., hello_world v1.0 and v2.0 with different Python function names) are treated as unversioned duplicates, and the deduplication picks one arbitrarily instead of by version ordering.
  4. Version-targeted call_tool fails — Clients sending _meta.fastmcp.version in call_tool requests won't match specific versions since the server doesn't know tools have versions.

Reproduction

# tools/hello_world.py
from fastmcp.tools import tool

@tool(name="hello_world", version="1.0", description="v1")
def hello_world_v1(name: str) -> str:
    return f"Hello {name}"

@tool(name="hello_world", version="2.0", description="v2")
def hello_world_v2(name: str, age: int) -> str:
    return f"Hello {name}, age {age}"
# client.py
from fastmcp import Client
import asyncio

async def main():
    async with Client("http://localhost:3000/mcp") as client:
        tools = await client.list_tools()
        for t in tools:
            meta = t.meta or {}
            fastmcp = meta.get("fastmcp", {})
            print(f"{t.name}: version={fastmcp.get('version')}, versions={fastmcp.get('versions')}")
            # Expected: hello_world: version=2.0, versions=['2.0', '1.0']
            # Actual:   hello_world: version=None, versions=None

asyncio.run(main())

Fix

Add version=meta.version to all three *.from_function() calls in extract_components():

 if isinstance(meta, ToolMeta):
     tool = Tool.from_function(
         obj,
         name=meta.name,
+        version=meta.version,
         title=meta.title,
         ...
     )
 elif isinstance(meta, ResourceMeta):
     ...
     resource = ResourceTemplate.from_function(
         fn=obj,
+        version=meta.version,
         ...
     )
     ...
     resource = Resource.from_function(
         fn=obj,
+        version=meta.version,
         ...
     )
 elif isinstance(meta, PromptMeta):
     prompt = Prompt.from_function(
         obj,
+        version=meta.version,
         ...
     )
  • My change closes #(issue number)
  • I have followed the repository's development workflow
  • I have tested my changes manually and by adding relevant tests
  • I have performed all required documentation updates

Review Checklist

  • I have self-reviewed my changes
  • My Pull Request is ready for review

@marvin-context-protocol marvin-context-protocol bot added bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality. server Related to FastMCP server implementation or server-side functionality. tests labels Mar 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality. server Related to FastMCP server implementation or server-side functionality. tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant