Publish FastMCP 4.0.0b1 docs to gofastmcp.com (#4695)

This commit is contained in:
Jeremiah Lowin 2026-07-28 17:26:15 -04:00 committed by GitHub
commit 0747ce0bc1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
220 changed files with 12093 additions and 8314 deletions

View file

@ -1,71 +1,97 @@
import json
from typing import cast
from unittest.mock import AsyncMock
import mcp_types
import pytest
from mcp_types import TextContent
from pydantic_core import to_json
from fastmcp import Client, Context, FastMCP
from fastmcp.client.sampling import RequestContext, SamplingMessage, SamplingParams
from fastmcp.server.sampling import SamplingResult, SamplingTool
from fastmcp.utilities.types import Image
async def _sample(
context: Context,
messages: list[SamplingMessage],
*,
system_prompt: str | None = None,
) -> str:
"""Issue a handshake-era `sampling/createMessage` request from a server.
`Context` has no `sample()` server-initiated sampling is not part of
FastMCP's server API. These tests cover the *client* side, which must keep
answering a legacy server, so the stand-in server reaches the SDK session
directly.
"""
result = await context.session.create_message( # ty: ignore[deprecated]
messages=messages,
system_prompt=system_prompt,
max_tokens=512,
related_request_id=context.origin_request_id,
)
assert isinstance(result.content, TextContent)
return result.content.text
@pytest.fixture
def fastmcp_server():
mcp = FastMCP()
@mcp.tool
async def simple_sample(message: str, context: Context) -> str:
result = await context.sample("Hello, world!")
assert isinstance(result, SamplingResult)
assert result.text is not None
return result.text
return await _sample(
context,
[
SamplingMessage(
role="user",
content=TextContent(type="text", text="Hello, world!"),
)
],
)
@mcp.tool
async def sample_with_system_prompt(message: str, context: Context) -> str:
result = await context.sample("Hello, world!", system_prompt="You love FastMCP")
assert isinstance(result, SamplingResult)
assert result.text is not None
return result.text
return await _sample(
context,
[
SamplingMessage(
role="user",
content=TextContent(type="text", text="Hello, world!"),
)
],
system_prompt="You love FastMCP",
)
@mcp.tool
async def sample_with_messages(message: str, context: Context) -> str:
result = await context.sample(
return await _sample(
context,
[
"Hello!",
SamplingMessage(
role="user", content=TextContent(type="text", text="Hello!")
),
SamplingMessage(
role="assistant",
content=TextContent(
type="text", text="How can I assist you today?"
),
role="assistant",
),
]
],
)
assert isinstance(result, SamplingResult)
assert result.text is not None
return result.text
@mcp.tool
async def sample_with_image(image_bytes: bytes, context: Context) -> str:
image = Image(data=image_bytes)
result = await context.sample(
return await _sample(
context,
[
SamplingMessage(
content=TextContent(type="text", text="What's in this image?"),
role="user",
),
SamplingMessage(
content=image.to_image_content(),
role="user",
),
]
SamplingMessage(content=image.to_image_content(), role="user"),
],
)
assert isinstance(result, SamplingResult)
assert result.text is not None
return result.text
return mcp
@ -123,26 +149,6 @@ async def test_sampling_with_messages(fastmcp_server: FastMCP):
assert result.data == "I need to think."
async def test_sampling_with_fallback(fastmcp_server: FastMCP):
openai_sampling_handler = AsyncMock(return_value="But I need to think")
fastmcp_server = FastMCP(
sampling_handler=openai_sampling_handler,
)
@fastmcp_server.tool
async def sample_with_fallback(context: Context) -> str:
sampling_result = await context.sample("Do not think.")
return cast(TextContent, sampling_result).text
client = Client(fastmcp_server)
async with client:
call_tool_result = await client.call_tool("sample_with_fallback")
assert call_tool_result.data == "But I need to think"
async def test_sampling_with_image(fastmcp_server: FastMCP):
def sampling_handler(
messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext
@ -193,8 +199,6 @@ class TestSamplingDefaultCapabilities:
{"sampling": {"tools": {}}}, ensuring compatibility with servers
that don't recognize the tools sub-field (e.g. older Java MCP SDK).
"""
import mcp_types
server = FastMCP()
def handler(
@ -209,8 +213,6 @@ class TestSamplingDefaultCapabilities:
async def test_set_sampling_callback_default_capabilities_omit_tools(self):
"""set_sampling_callback should also default to no tools capability."""
import mcp_types
server = FastMCP()
client = Client(server)
client.set_sampling_callback(lambda msgs, params, ctx: "ok")
@ -220,8 +222,6 @@ class TestSamplingDefaultCapabilities:
async def test_explicit_tools_capability_is_preserved(self):
"""Explicitly passing tools capability should be respected."""
import mcp_types
server = FastMCP()
def handler(
@ -238,118 +238,3 @@ class TestSamplingDefaultCapabilities:
caps = client._session_kwargs["sampling_capabilities"]
assert isinstance(caps, mcp_types.SamplingCapability)
assert caps.tools is not None
class TestSamplingWithTools:
"""Tests for sampling with tools functionality."""
async def test_sampling_with_tools_requires_capability(self):
"""Test that sampling with tools raises error when client lacks capability."""
import mcp_types
from fastmcp.exceptions import ToolError
server = FastMCP()
def search(query: str) -> str:
"""Search the web."""
return f"Results for: {query}"
@server.tool
async def sample_with_tool(context: Context) -> str:
# This should fail because the client doesn't advertise tools capability
result = await context.sample(
messages="Search for Python tutorials",
tools=[search],
)
return str(result)
def sampling_handler(
messages: list[SamplingMessage], params: SamplingParams, ctx: RequestContext
) -> str:
return "Response"
# Explicitly disable tools capability by passing SamplingCapability without tools
async with Client(
server,
mode="legacy",
sampling_handler=sampling_handler,
sampling_capabilities=mcp_types.SamplingCapability(), # No tools
) as client:
with pytest.raises(ToolError, match="sampling.tools capability"):
await client.call_tool("sample_with_tool", {})
async def test_sampling_with_tools_fallback_handler_can_return_string(self):
"""Test that fallback handler can return a string even when tools are provided.
The LLM might choose not to use any tools and just return a text response.
"""
# This handler returns a string - valid even when tools are provided
simple_handler = AsyncMock(return_value="Direct response without tools")
mcp = FastMCP(sampling_handler=simple_handler)
def search(query: str) -> str:
"""Search the web."""
return f"Results for: {query}"
@mcp.tool
async def sample_with_tool(context: Context) -> str:
result = await context.sample(
messages="Search for Python tutorials",
tools=[search],
)
return result.text or "no text"
# Client without sampling handler - will use server's fallback
async with Client(mcp) as client:
result = await client.call_tool("sample_with_tool", {})
# Handler returned string directly, which is treated as final text response
assert result.data == "Direct response without tools"
def test_sampling_tool_schema(self):
"""Test that SamplingTool generates correct schema."""
def search(query: str, limit: int = 10) -> str:
"""Search the web for results."""
return f"Results for: {query}"
tool = SamplingTool.from_function(search)
assert tool.name == "search"
assert tool.description == "Search the web for results."
assert "query" in tool.parameters.get("properties", {})
assert "limit" in tool.parameters.get("properties", {})
async def test_sampling_tool_run(self):
"""Test that SamplingTool.run() executes correctly."""
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
tool = SamplingTool.from_function(add)
result = await tool.run({"a": 5, "b": 3})
assert result == 8
async def test_sampling_tool_run_async(self):
"""Test that SamplingTool.run() works with async functions."""
async def async_multiply(a: int, b: int) -> int:
"""Multiply two numbers."""
return a * b
tool = SamplingTool.from_function(async_multiply)
result = await tool.run({"a": 4, "b": 7})
assert result == 28
def test_tool_choice_parameter(self):
"""Test that tool_choice parameter accepts string literals."""
from fastmcp.server.context import ToolChoiceOption
# Verify ToolChoiceOption type accepts the valid string values
choices: list[ToolChoiceOption] = ["auto", "required", "none"]
assert len(choices) == 3
assert "auto" in choices
assert "required" in choices
assert "none" in choices