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
18 changes: 17 additions & 1 deletion docs/development/v3-notes/v3-features.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,23 @@ This document tracks major features in FastMCP v3.0 for release notes preparatio

## 3.0.0rc1

### SamplingTool Conversion Helpers

Server tools (FunctionTool and TransformedTool) can now be passed directly to sampling methods via `SamplingTool.from_callable_tool()` ([#3062](https://github.com/jlowin/fastmcp/pull/3062)). Previously, tools defined with `@mcp.tool` had to be recreated as functions for use in `ctx.sample()`. Now `ctx.sample()` and `ctx.sample_step()` accept these tool instances directly.

```python
@mcp.tool
def search(query: str) -> str:
"""Search the web."""
return do_search(query)

# Use tool directly in sampling
result = await ctx.sample(
"Research Python frameworks",
tools=[search] # FunctionTool works directly!
)
```

### Concurrent Tool Execution in Sampling

When an LLM returns multiple tool calls in a single sampling response, they can now be executed concurrently ([#3022](https://github.com/jlowin/fastmcp/pull/3022)). Default behavior remains sequential; opt in with `tool_concurrency`. Tools can declare `sequential=True` to force sequential execution even when concurrency is enabled.
Expand Down Expand Up @@ -109,7 +126,6 @@ The `_deprecated_settings` attribute and `.settings` property are also removed.
### Breaking: `ui=` Renamed to `app=`

The MCP Apps decorator parameter has been renamed from `ui=ToolUI(...)` / `ui=ResourceUI(...)` to `app=AppConfig(...)` ([#3117](https://github.com/jlowin/fastmcp/pull/3117)). `ToolUI` and `ResourceUI` are consolidated into a single `AppConfig` class. Wire format is unchanged. See the MCP Apps section under beta2 for full details.

## 3.0.0beta2

### CLI: `fastmcp list` and `fastmcp call`
Expand Down
41 changes: 26 additions & 15 deletions docs/python-sdk/fastmcp-server-sampling-run.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Sampling types and helper functions for FastMCP servers.

## Functions

### `determine_handler_mode` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L130" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `determine_handler_mode` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L132" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

```python
determine_handler_mode(context: Context, needs_tools: bool) -> bool
Expand All @@ -30,7 +30,7 @@ Determine whether to use fallback handler or client for sampling.
- `ValueError`: If client lacks required capability and no fallback configured.


### `call_sampling_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L189" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `call_sampling_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L191" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

```python
call_sampling_handler(context: Context, messages: list[SamplingMessage]) -> CreateMessageResult | CreateMessageResultWithTools
Expand All @@ -44,7 +44,7 @@ sampling_handler is set via determine_handler_mode(). The checks below are
safeguards against internal misuse.


### `execute_tools` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L240" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `execute_tools` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L242" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

```python
execute_tools(tool_calls: list[ToolUseContent], tool_map: dict[str, SamplingTool], mask_error_details: bool = False, tool_concurrency: int | None = None) -> list[ToolResultContent]
Expand All @@ -71,7 +71,7 @@ regardless of this setting.
- List of tool result content blocks in the same order as tool_calls.


### `prepare_messages` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L350" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `prepare_messages` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L352" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

```python
prepare_messages(messages: str | Sequence[str | SamplingMessage]) -> list[SamplingMessage]
Expand All @@ -81,17 +81,28 @@ prepare_messages(messages: str | Sequence[str | SamplingMessage]) -> list[Sampli
Convert various message formats to a list of SamplingMessage objects.


### `prepare_tools` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L369" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `prepare_tools` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L371" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

```python
prepare_tools(tools: Sequence[SamplingTool | Callable[..., Any]] | None) -> list[SamplingTool] | None
prepare_tools(tools: Sequence[SamplingTool | FunctionTool | TransformedTool | Callable[..., Any]] | None) -> list[SamplingTool] | None
```


Convert tools to SamplingTool objects.

Accepts SamplingTool instances, FunctionTool instances, TransformedTool instances,
or plain callable functions. FunctionTool and TransformedTool are converted using
from_callable_tool(), while plain functions use from_function().

### `extract_tool_calls` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L388" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
**Args:**
- `tools`: Sequence of tools to prepare. Can be SamplingTool, FunctionTool,
TransformedTool, or plain callable functions.

**Returns:**
- List of SamplingTool instances, or None if tools is None.


### `extract_tool_calls` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L407" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

```python
extract_tool_calls(response: CreateMessageResult | CreateMessageResultWithTools) -> list[ToolUseContent]
Expand All @@ -101,7 +112,7 @@ extract_tool_calls(response: CreateMessageResult | CreateMessageResultWithTools)
Extract tool calls from a response.


### `create_final_response_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L400" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `create_final_response_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L419" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

```python
create_final_response_tool(result_type: type) -> SamplingTool
Expand All @@ -114,7 +125,7 @@ This tool is used to capture structured responses from the LLM.
The tool's schema is derived from the result_type.


### `sample_step_impl` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L436" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `sample_step_impl` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L455" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

```python
sample_step_impl(context: Context, messages: str | Sequence[str | SamplingMessage]) -> SampleStep
Expand All @@ -127,7 +138,7 @@ Make a single LLM sampling call. This is a stateless function that makes
exactly one LLM call and optionally executes any requested tools.


### `sample_impl` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L552" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `sample_impl` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L572" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

```python
sample_impl(context: Context, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[ResultT]
Expand All @@ -143,7 +154,7 @@ provides a final text response.

## Classes

### `SamplingResult` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L52" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `SamplingResult` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L54" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>


Result of a sampling operation.
Expand All @@ -154,7 +165,7 @@ Result of a sampling operation.
- `history`: All messages exchanged during sampling.


### `SampleStep` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L67" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `SampleStep` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L69" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>


Result of a single sampling call.
Expand All @@ -164,7 +175,7 @@ Represents what the LLM returned in this step plus the message history.

**Methods:**

#### `is_tool_use` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L77" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `is_tool_use` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L79" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

```python
is_tool_use(self) -> bool
Expand All @@ -173,7 +184,7 @@ is_tool_use(self) -> bool
True if the LLM is requesting tool execution.


#### `text` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L84" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `text` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L86" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

```python
text(self) -> str | None
Expand All @@ -182,7 +193,7 @@ text(self) -> str | None
Extract text from the response, if available.


#### `tool_calls` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L97" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `tool_calls` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L99" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

```python
tool_calls(self) -> list[ToolUseContent]
Expand Down
27 changes: 24 additions & 3 deletions docs/python-sdk/fastmcp-server-sampling-sampling_tool.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ SamplingTool for use during LLM sampling requests.

## Classes

### `SamplingTool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/sampling_tool.py#L16" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `SamplingTool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/sampling_tool.py#L20" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>


A tool that can be used during LLM sampling.
Expand All @@ -37,7 +37,7 @@ Create a SamplingTool explicitly when you need custom name/description:

**Methods:**

#### `run` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/sampling_tool.py#L47" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `run` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/sampling_tool.py#L51" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

```python
run(self, arguments: dict[str, Any] | None = None) -> Any
Expand All @@ -52,7 +52,7 @@ Execute the tool with the given arguments.
- The result of executing the tool function.


#### `from_function` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/sampling_tool.py#L77" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `from_function` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/sampling_tool.py#L81" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

```python
from_function(cls, fn: Callable[..., Any]) -> SamplingTool
Expand All @@ -78,3 +78,24 @@ concurrently. Defaults to False.
**Raises:**
- `ValueError`: If the function is a lambda without a name override.


#### `from_callable_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/sampling_tool.py#L123" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

```python
from_callable_tool(cls, tool: FunctionTool | TransformedTool) -> SamplingTool
```

Create a SamplingTool from a FunctionTool or TransformedTool.

Reuses existing server tools in sampling contexts. For TransformedTool,
the tool's .run() method is used to ensure proper argument transformation,
and the ToolResult is automatically unwrapped.

**Args:**
- `tool`: A FunctionTool or TransformedTool to convert.
- `name`: Optional name override. Defaults to tool.name.
- `description`: Optional description override. Defaults to tool.description.

**Raises:**
- `TypeError`: If the tool is not a FunctionTool or TransformedTool.

31 changes: 26 additions & 5 deletions src/fastmcp/server/sampling/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
from fastmcp import settings
from fastmcp.exceptions import ToolError
from fastmcp.server.sampling.sampling_tool import SamplingTool
from fastmcp.tools.function_tool import FunctionTool
from fastmcp.tools.tool_transform import TransformedTool
from fastmcp.utilities.async_utils import gather
from fastmcp.utilities.json_schema import compress_schema
from fastmcp.utilities.logging import get_logger
Expand Down Expand Up @@ -367,20 +369,37 @@ def prepare_messages(


def prepare_tools(
tools: Sequence[SamplingTool | Callable[..., Any]] | None,
tools: Sequence[SamplingTool | FunctionTool | TransformedTool | Callable[..., Any]]
| None,
) -> list[SamplingTool] | None:
"""Convert tools to SamplingTool objects."""
"""Convert tools to SamplingTool objects.

Accepts SamplingTool instances, FunctionTool instances, TransformedTool instances,
or plain callable functions. FunctionTool and TransformedTool are converted using
from_callable_tool(), while plain functions use from_function().

Args:
tools: Sequence of tools to prepare. Can be SamplingTool, FunctionTool,
TransformedTool, or plain callable functions.

Returns:
List of SamplingTool instances, or None if tools is None.
"""
if tools is None:
return None

sampling_tools: list[SamplingTool] = []
for t in tools:
if isinstance(t, SamplingTool):
sampling_tools.append(t)
elif isinstance(t, (FunctionTool, TransformedTool)):
sampling_tools.append(SamplingTool.from_callable_tool(t))
elif callable(t):
sampling_tools.append(SamplingTool.from_function(t))
else:
raise TypeError(f"Expected SamplingTool or callable, got {type(t)}")
raise TypeError(
f"Expected SamplingTool, FunctionTool, TransformedTool, or callable, got {type(t)}"
)

return sampling_tools if sampling_tools else None

Expand Down Expand Up @@ -441,7 +460,8 @@ async def sample_step_impl(
temperature: float | None = None,
max_tokens: int | None = None,
model_preferences: ModelPreferences | str | list[str] | None = None,
tools: Sequence[SamplingTool | Callable[..., Any]] | None = None,
tools: Sequence[SamplingTool | FunctionTool | TransformedTool | Callable[..., Any]]
| None = None,
tool_choice: ToolChoiceOption | str | None = None,
auto_execute_tools: bool = True,
mask_error_details: bool | None = None,
Expand Down Expand Up @@ -557,7 +577,8 @@ async def sample_impl(
temperature: float | None = None,
max_tokens: int | None = None,
model_preferences: ModelPreferences | str | list[str] | None = None,
tools: Sequence[SamplingTool | Callable[..., Any]] | None = None,
tools: Sequence[SamplingTool | FunctionTool | TransformedTool | Callable[..., Any]]
| None = None,
result_type: type[ResultT] | None = None,
mask_error_details: bool | None = None,
tool_concurrency: int | None = None,
Expand Down
67 changes: 67 additions & 0 deletions src/fastmcp/server/sampling/sampling_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@
from collections.abc import Callable
from typing import Any

from mcp.types import TextContent
from mcp.types import Tool as SDKTool
from pydantic import ConfigDict

from fastmcp.tools.function_parsing import ParsedFunction
from fastmcp.tools.function_tool import FunctionTool
from fastmcp.tools.tool import ToolResult
from fastmcp.tools.tool_transform import TransformedTool
from fastmcp.utilities.types import FastMCPBaseModel


Expand Down Expand Up @@ -114,3 +118,66 @@ def from_function(
fn=parsed.fn,
sequential=sequential,
)

@classmethod
def from_callable_tool(
cls,
tool: FunctionTool | TransformedTool,
*,
name: str | None = None,
description: str | None = None,
) -> SamplingTool:
"""Create a SamplingTool from a FunctionTool or TransformedTool.

Reuses existing server tools in sampling contexts. For TransformedTool,
the tool's .run() method is used to ensure proper argument transformation,
and the ToolResult is automatically unwrapped.

Args:
tool: A FunctionTool or TransformedTool to convert.
name: Optional name override. Defaults to tool.name.
description: Optional description override. Defaults to tool.description.

Raises:
TypeError: If the tool is not a FunctionTool or TransformedTool.
"""
# Validate that the tool is a supported type
if not isinstance(tool, (FunctionTool, TransformedTool)):
raise TypeError(
f"Expected FunctionTool or TransformedTool, got {type(tool).__name__}. "
"Only callable tools can be converted to SamplingTools."
)

# Both FunctionTool and TransformedTool need .run() to ensure proper
# result processing (serializers, output_schema, wrap-result flags)
async def wrapper(**kwargs: Any) -> Any:
result = await tool.run(kwargs)
# Unwrap ToolResult - extract the actual value
if isinstance(result, ToolResult):
# If there's structured_content, use that
if result.structured_content is not None:
# Check tool's schema - this is the source of truth
if tool.output_schema and tool.output_schema.get(
"x-fastmcp-wrap-result"
):
# Tool wraps results: {"result": value} -> value
return result.structured_content.get("result")
else:
# No wrapping: use structured_content directly
return result.structured_content
# Otherwise, extract from text content
if result.content and len(result.content) > 0:
first_content = result.content[0]
if isinstance(first_content, TextContent):
return first_content.text
return result

fn = wrapper

# Extract the callable function, name, description, and parameters
return cls(
name=name or tool.name,
description=description or tool.description,
parameters=tool.parameters,
fn=fn,
)
Loading