Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/development/upgrade-guide.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,29 @@ tag: NEW

This guide provides migration instructions for breaking changes and major updates when upgrading between FastMCP versions.

## v3.0.0

### Component Lookup Method Parameter Names

The server lookup methods now use semantic parameter names instead of generic `key`:

- `FastMCP.get_tool(name=...)` (was `key`)
- `FastMCP.get_resource(uri=...)` (was `key`)
- `FastMCP.get_resource_template(uri=...)` (was `key`)
- `FastMCP.get_prompt(name=...)` (was `key`)

If you were passing arguments positionally, no change is needed. If you were using keyword arguments:

<CodeGroup>
```python Before
tool = await mcp.get_tool(key="my_tool")
```

```python After
tool = await mcp.get_tool(name="my_tool")
```
</CodeGroup>

## v2.14.0

### OpenAPI Parser Promotion
Expand Down
88 changes: 42 additions & 46 deletions src/fastmcp/contrib/component_manager/component_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,125 +58,121 @@ class ComponentService:
def __init__(self, server: FastMCP):
self._server = server

async def _enable_tool(self, key: str) -> Tool:
async def _enable_tool(self, name: str) -> Tool:
"""Handle 'enableTool' requests.

Args:
key: The key of the tool to enable
name: The name of the tool to enable

Returns:
The tool that was enabled
"""
logger.debug("Enabling tool: %s", key)
logger.debug("Enabling tool: %s", name)

# 1. Check local tools first. The server will have already applied its filter.
if key in self._server._local_provider._tools:
tool: Tool = await self._server.get_tool(key)
if Tool.make_key(name) in self._server._local_provider._components:
tool: Tool = await self._server.get_tool(name)
tool.enable()
return tool

# 2. Check mounted servers via FastMCPProvider/TransformingProvider
for provider in self._server._providers:
result = _get_mounted_server_and_key(provider, key, "tool")
result = _get_mounted_server_and_key(provider, name, "tool")
if result is not None:
server, unprefixed = result
mounted_service = ComponentService(server)
tool = await mounted_service._enable_tool(unprefixed)
return tool
raise NotFoundError(f"Unknown tool: {key}")
raise NotFoundError(f"Unknown tool: {name}")

async def _disable_tool(self, key: str) -> Tool:
async def _disable_tool(self, name: str) -> Tool:
"""Handle 'disableTool' requests.

Args:
key: The key of the tool to disable
name: The name of the tool to disable

Returns:
The tool that was disabled
"""
logger.debug("Disable tool: %s", key)
logger.debug("Disable tool: %s", name)

# 1. Check local tools first. The server will have already applied its filter.
if key in self._server._local_provider._tools:
tool: Tool = await self._server.get_tool(key)
if Tool.make_key(name) in self._server._local_provider._components:
tool: Tool = await self._server.get_tool(name)
tool.disable()
return tool

# 2. Check mounted servers via FastMCPProvider/TransformingProvider
for provider in self._server._providers:
result = _get_mounted_server_and_key(provider, key, "tool")
result = _get_mounted_server_and_key(provider, name, "tool")
if result is not None:
server, unprefixed = result
mounted_service = ComponentService(server)
tool = await mounted_service._disable_tool(unprefixed)
return tool
raise NotFoundError(f"Unknown tool: {key}")
raise NotFoundError(f"Unknown tool: {name}")

async def _enable_resource(self, key: str) -> Resource | ResourceTemplate:
async def _enable_resource(self, uri: str) -> Resource | ResourceTemplate:
"""Handle 'enableResource' requests.

Args:
key: The key of the resource to enable
uri: The URI of the resource to enable

Returns:
The resource that was enabled
"""
logger.debug("Enabling resource: %s", key)

# 1. Check local resources first. The server will have already applied its filter.
if key in self._server._local_provider._resources:
resource: Resource = await self._server.get_resource(key)
resource.enable()
return resource
if key in self._server._local_provider._templates:
template: ResourceTemplate = await self._server.get_resource_template(key)
template.enable()
return template
logger.debug("Enabling resource: %s", uri)

# 1. Check local components first (try resource, then template)
component = self._server._local_provider._get_component(
Resource.make_key(uri)
) or self._server._local_provider._get_component(ResourceTemplate.make_key(uri))
if component is not None:
component.enable()
return component # type: ignore[return-value]

# 2. Check mounted servers via FastMCPProvider/TransformingProvider
for provider in self._server._providers:
result = _get_mounted_server_and_key(provider, key, "resource")
result = _get_mounted_server_and_key(provider, uri, "resource")
if result is not None:
server, unprefixed = result
mounted_service = ComponentService(server)
mounted_resource: (
Resource | ResourceTemplate
) = await mounted_service._enable_resource(unprefixed)
return mounted_resource
raise NotFoundError(f"Unknown resource: {key}")
raise NotFoundError(f"Unknown resource: {uri}")

async def _disable_resource(self, key: str) -> Resource | ResourceTemplate:
async def _disable_resource(self, uri: str) -> Resource | ResourceTemplate:
"""Handle 'disableResource' requests.

Args:
key: The key of the resource to disable
uri: The URI of the resource to disable

Returns:
The resource that was disabled
"""
logger.debug("Disable resource: %s", key)

# 1. Check local resources first. The server will have already applied its filter.
if key in self._server._local_provider._resources:
resource: Resource = await self._server.get_resource(key)
resource.disable()
return resource
if key in self._server._local_provider._templates:
template: ResourceTemplate = await self._server.get_resource_template(key)
template.disable()
return template
logger.debug("Disable resource: %s", uri)

# 1. Check local components first (try resource, then template)
component = self._server._local_provider._get_component(
Resource.make_key(uri)
) or self._server._local_provider._get_component(ResourceTemplate.make_key(uri))
if component is not None:
component.disable()
return component # type: ignore[return-value]

# 2. Check mounted servers via FastMCPProvider/TransformingProvider
for provider in self._server._providers:
result = _get_mounted_server_and_key(provider, key, "resource")
result = _get_mounted_server_and_key(provider, uri, "resource")
if result is not None:
server, unprefixed = result
mounted_service = ComponentService(server)
mounted_resource: (
Resource | ResourceTemplate
) = await mounted_service._disable_resource(unprefixed)
return mounted_resource
raise NotFoundError(f"Unknown resource: {key}")
raise NotFoundError(f"Unknown resource: {uri}")

async def _enable_prompt(self, key: str) -> Prompt:
"""Handle 'enablePrompt' requests.
Expand All @@ -190,7 +186,7 @@ async def _enable_prompt(self, key: str) -> Prompt:
logger.debug("Enabling prompt: %s", key)

# 1. Check local prompts first. The server will have already applied its filter.
if key in self._server._local_provider._prompts:
if Prompt.make_key(key) in self._server._local_provider._components:
prompt: Prompt = await self._server.get_prompt(key)
prompt.enable()
return prompt
Expand All @@ -216,7 +212,7 @@ async def _disable_prompt(self, key: str) -> Prompt:
"""

# 1. Check local prompts first. The server will have already applied its filter.
if key in self._server._local_provider._prompts:
if Prompt.make_key(key) in self._server._local_provider._components:
prompt: Prompt = await self._server.get_prompt(key)
prompt.disable()
return prompt
Expand Down
4 changes: 3 additions & 1 deletion src/fastmcp/prompts/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import json
import warnings
from collections.abc import Awaitable, Callable, Sequence
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, ClassVar

import pydantic_core

Expand Down Expand Up @@ -118,6 +118,8 @@ def to_mcp_prompt_result(self) -> GetPromptResult:
class Prompt(FastMCPComponent):
"""A prompt template that can be rendered with parameters."""

KEY_PREFIX: ClassVar[str] = "prompt"

arguments: list[PromptArgument] | None = Field(
default=None, description="Arguments that can be passed to the prompt"
)
Expand Down
8 changes: 5 additions & 3 deletions src/fastmcp/resources/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import inspect
import warnings
from collections.abc import Callable
from typing import TYPE_CHECKING, Annotated, Any
from typing import TYPE_CHECKING, Annotated, Any, ClassVar

import mcp.types

Expand Down Expand Up @@ -137,6 +137,8 @@ def to_mcp_resource_contents(
class Resource(FastMCPComponent):
"""Base class for all resources."""

KEY_PREFIX: ClassVar[str] = "resource"

model_config = ConfigDict(validate_default=True)

uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] = Field(
Expand Down Expand Up @@ -300,8 +302,8 @@ def __repr__(self) -> str:

@property
def key(self) -> str:
"""The lookup key for this resource. Returns str(uri)."""
return str(self.uri)
"""The globally unique lookup key for this resource."""
return self.make_key(str(self.uri))

def register_with_docket(self, docket: Docket) -> None:
"""Register this resource with docket for background execution."""
Expand Down
8 changes: 5 additions & 3 deletions src/fastmcp/resources/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import inspect
import re
from collections.abc import Callable
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, ClassVar
from urllib.parse import parse_qs, unquote

import mcp.types
Expand Down Expand Up @@ -97,6 +97,8 @@ def match_uri_template(uri: str, uri_template: str) -> dict[str, str] | None:
class ResourceTemplate(FastMCPComponent):
"""A template for dynamically creating resources."""

KEY_PREFIX: ClassVar[str] = "template"

uri_template: str = Field(
description="URI template with parameters (e.g. weather://{city}/current)"
)
Expand Down Expand Up @@ -265,8 +267,8 @@ def from_mcp_template(cls, mcp_template: SDKResourceTemplate) -> ResourceTemplat

@property
def key(self) -> str:
"""The lookup key for this template. Returns uri_template."""
return self.uri_template
"""The globally unique lookup key for this template."""
return self.make_key(self.uri_template)

def register_with_docket(self, docket: Docket) -> None:
"""Register this template with docket for background execution."""
Expand Down
Loading
Loading