Merge branch 'protocol-update' into elicitation

This commit is contained in:
Jeremiah Lowin 2025-06-20 10:08:08 -04:00
commit 1d05578b3f
12 changed files with 46 additions and 40 deletions

View file

@ -9,6 +9,7 @@ import httpx
import mcp.types
from exceptiongroup import catch
from mcp import ClientSession
from mcp.types import ContentBlock
from pydantic import AnyUrl
import fastmcp
@ -30,7 +31,6 @@ from fastmcp.exceptions import ToolError
from fastmcp.server import FastMCP
from fastmcp.utilities.exceptions import get_catch_handlers
from fastmcp.utilities.mcp_config import MCPConfig
from fastmcp.utilities.types import MCPContent
from .transports import (
ClientTransportT,
@ -673,7 +673,7 @@ class Client(Generic[ClientTransportT]):
arguments: dict[str, Any] | None = None,
timeout: datetime.timedelta | float | int | None = None,
progress_handler: ProgressHandler | None = None,
) -> list[MCPContent]:
) -> list[ContentBlock]:
"""Call a tool on the server.
Unlike call_tool_mcp, this method raises a ToolError if the tool call results in an error.

View file

@ -8,9 +8,9 @@ from collections.abc import Awaitable, Callable, Sequence
from typing import TYPE_CHECKING, Any
import pydantic_core
from mcp.types import ContentBlock, PromptMessage, Role, TextContent
from mcp.types import Prompt as MCPPrompt
from mcp.types import PromptArgument as MCPPromptArgument
from mcp.types import PromptMessage, Role, TextContent
from pydantic import Field, TypeAdapter, validate_call
from fastmcp.exceptions import PromptError
@ -20,7 +20,6 @@ from fastmcp.utilities.json_schema import compress_schema
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import (
FastMCPBaseModel,
MCPContent,
find_kwarg_by_type,
get_cached_typeadapter,
)
@ -33,7 +32,7 @@ logger = get_logger(__name__)
def Message(
content: str | MCPContent, role: Role | None = None, **kwargs: Any
content: str | ContentBlock, role: Role | None = None, **kwargs: Any
) -> PromptMessage:
"""A user-friendly constructor for PromptMessage."""
if isinstance(content, str):

View file

@ -12,6 +12,7 @@ from mcp.server.lowlevel.helper_types import ReadResourceContents
from mcp.server.lowlevel.server import request_ctx
from mcp.shared.context import RequestContext
from mcp.types import (
ContentBlock,
CreateMessageResult,
ModelHint,
ModelPreferences,
@ -33,7 +34,7 @@ from fastmcp.server.elicitation import (
)
from fastmcp.server.server import FastMCP
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import MCPContent, get_cached_typeadapter
from fastmcp.utilities.types import get_cached_typeadapter
logger = get_logger(__name__)
@ -255,7 +256,7 @@ class Context:
temperature: float | None = None,
max_tokens: int | None = None,
model_preferences: ModelPreferences | str | list[str] | None = None,
) -> MCPContent:
) -> ContentBlock:
"""
Send a sampling request to the client and await the response.

View file

@ -13,7 +13,7 @@ from re import Pattern
from typing import TYPE_CHECKING, Any, Literal
import httpx
from mcp.types import ToolAnnotations
from mcp.types import ContentBlock, ToolAnnotations
from pydantic.networks import AnyUrl
import fastmcp
@ -29,7 +29,6 @@ from fastmcp.utilities.openapi import (
_combine_schemas,
format_description_with_responses,
)
from fastmcp.utilities.types import MCPContent
if TYPE_CHECKING:
from fastmcp.server import Context
@ -255,7 +254,7 @@ class OpenAPITool(Tool):
"""Custom representation to prevent recursion errors when printing."""
return f"OpenAPITool(name={self.name!r}, method={self._route.method}, path={self._route.path})"
async def run(self, arguments: dict[str, Any]) -> list[MCPContent]:
async def run(self, arguments: dict[str, Any]) -> list[ContentBlock]:
"""Execute the HTTP request based on the route configuration."""
# Prepare URL

View file

@ -8,6 +8,7 @@ from mcp.shared.exceptions import McpError
from mcp.types import (
METHOD_NOT_FOUND,
BlobResourceContents,
ContentBlock,
GetPromptResult,
TextResourceContents,
)
@ -25,7 +26,6 @@ from fastmcp.server.server import FastMCP
from fastmcp.tools.tool import Tool
from fastmcp.tools.tool_manager import ToolManager
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import MCPContent
if TYPE_CHECKING:
from fastmcp.server import Context
@ -67,7 +67,9 @@ class ProxyToolManager(ToolManager):
tools_dict = await self.get_tools()
return list(tools_dict.values())
async def call_tool(self, key: str, arguments: dict[str, Any]) -> list[MCPContent]:
async def call_tool(
self, key: str, arguments: dict[str, Any]
) -> list[ContentBlock]:
"""Calls a tool, trying local/mounted first, then proxy if not found."""
try:
# First try local and mounted tools
@ -230,7 +232,7 @@ class ProxyTool(Tool):
self,
arguments: dict[str, Any],
context: Context | None = None,
) -> list[MCPContent]:
) -> list[ContentBlock]:
"""Executes the tool by making a call through the client."""
# This is where the remote execution logic lives.
async with self._client:

View file

@ -27,6 +27,7 @@ from mcp.server.lowlevel.server import Server as MCPServer
from mcp.server.stdio import stdio_server
from mcp.types import (
AnyFunction,
ContentBlock,
GetPromptResult,
ToolAnnotations,
)
@ -62,7 +63,6 @@ from fastmcp.utilities.cache import TimedCache
from fastmcp.utilities.components import FastMCPComponent
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.mcp_config import MCPConfig
from fastmcp.utilities.types import MCPContent
if TYPE_CHECKING:
from fastmcp.client import Client
@ -586,7 +586,7 @@ class FastMCP(Generic[LifespanResultT]):
async def _mcp_call_tool(
self, key: str, arguments: dict[str, Any]
) -> list[MCPContent]:
) -> list[ContentBlock]:
"""
Handle MCP 'callTool' requests.
@ -609,14 +609,16 @@ class FastMCP(Generic[LifespanResultT]):
except NotFoundError:
raise NotFoundError(f"Unknown tool: {key}")
async def _call_tool(self, key: str, arguments: dict[str, Any]) -> list[MCPContent]:
async def _call_tool(
self, key: str, arguments: dict[str, Any]
) -> list[ContentBlock]:
"""
Applies this server's middleware and delegates the filtered call to the manager.
"""
async def _handler(
context: MiddlewareContext[mcp.types.CallToolRequestParams],
) -> list[MCPContent]:
) -> list[ContentBlock]:
tool = await self._tool_manager.get_tool(context.message.name)
if not self._should_enable_component(tool):
raise NotFoundError(f"Unknown tool: {context.message.name!r}")

View file

@ -7,7 +7,7 @@ from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
import pydantic_core
from mcp.types import TextContent, ToolAnnotations
from mcp.types import ContentBlock, TextContent, ToolAnnotations
from mcp.types import Tool as MCPTool
from pydantic import Field
@ -20,7 +20,6 @@ from fastmcp.utilities.types import (
Audio,
File,
Image,
MCPContent,
find_kwarg_by_type,
get_cached_typeadapter,
)
@ -78,7 +77,7 @@ class Tool(FastMCPComponent):
enabled=enabled,
)
async def run(self, arguments: dict[str, Any]) -> list[MCPContent]:
async def run(self, arguments: dict[str, Any]) -> list[ContentBlock]:
"""Run the tool with arguments."""
raise NotImplementedError("Subclasses must implement run()")
@ -143,7 +142,7 @@ class FunctionTool(Tool):
enabled=enabled if enabled is not None else True,
)
async def run(self, arguments: dict[str, Any]) -> list[MCPContent]:
async def run(self, arguments: dict[str, Any]) -> list[ContentBlock]:
"""Run the tool with arguments."""
from fastmcp.server.context import Context
@ -264,12 +263,12 @@ def _convert_to_content(
result: Any,
serializer: Callable[[Any], str] | None = None,
_process_as_single_item: bool = False,
) -> list[MCPContent]:
) -> list[ContentBlock]:
"""Convert a result to a sequence of content objects."""
if result is None:
return []
if isinstance(result, MCPContent):
if isinstance(result, ContentBlock):
return [result]
if isinstance(result, Image):
@ -292,7 +291,7 @@ def _convert_to_content(
other_content = []
for item in result:
if isinstance(item, MCPContent | Image | Audio | File):
if isinstance(item, ContentBlock | Image | Audio | File):
mcp_types.append(_convert_to_content(item)[0])
else:
other_content.append(item)

View file

@ -4,14 +4,13 @@ import warnings
from collections.abc import Callable
from typing import TYPE_CHECKING, Any
from mcp.types import ToolAnnotations
from mcp.types import ContentBlock, ToolAnnotations
from fastmcp import settings
from fastmcp.exceptions import NotFoundError, ToolError
from fastmcp.settings import DuplicateBehavior
from fastmcp.tools.tool import Tool
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import MCPContent
if TYPE_CHECKING:
from fastmcp.server.server import MountedServer
@ -170,7 +169,9 @@ class ToolManager:
else:
raise NotFoundError(f"Tool {key!r} not found")
async def call_tool(self, key: str, arguments: dict[str, Any]) -> list[MCPContent]:
async def call_tool(
self, key: str, arguments: dict[str, Any]
) -> list[ContentBlock]:
"""
Internal API for servers: Finds and calls a tool, respecting the
filtered protocol path.

View file

@ -7,12 +7,12 @@ from dataclasses import dataclass
from types import EllipsisType
from typing import Any, Literal
from mcp.types import ToolAnnotations
from mcp.types import ContentBlock, ToolAnnotations
from pydantic import ConfigDict
from fastmcp.tools.tool import ParsedFunction, Tool
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import MCPContent, get_cached_typeadapter
from fastmcp.utilities.types import get_cached_typeadapter
logger = get_logger(__name__)
@ -202,7 +202,7 @@ class TransformedTool(Tool):
forwarding_fn: Callable[..., Any] # Always present, handles arg transformation
transform_args: dict[str, ArgTransform]
async def run(self, arguments: dict[str, Any]) -> list[MCPContent]:
async def run(self, arguments: dict[str, Any]) -> list[ContentBlock]:
"""Run the tool with context set for forward() functions.
This method executes the tool's function while setting up the context

View file

@ -7,7 +7,7 @@ from collections.abc import Callable
from functools import lru_cache
from pathlib import Path
from types import UnionType
from typing import Annotated, TypeAlias, TypeVar, Union, get_args, get_origin
from typing import Annotated, TypeVar, Union, get_args, get_origin
from mcp.types import (
Annotations,
@ -15,15 +15,12 @@ from mcp.types import (
BlobResourceContents,
EmbeddedResource,
ImageContent,
TextContent,
TextResourceContents, # Added import
TextResourceContents,
)
from pydantic import AnyUrl, BaseModel, ConfigDict, TypeAdapter, UrlConstraints
T = TypeVar("T")
MCPContent: TypeAlias = TextContent | ImageContent | AudioContent | EmbeddedResource
class FastMCPBaseModel(BaseModel):
"""Base model for FastMCP models."""

View file

@ -223,6 +223,8 @@ class TestTools:
assert tools[0].model_dump() == dict(
name="create_user_users_post",
meta=None,
title=None,
annotations=None,
description=IsStr(regex=r"^Create a new user\..*$", regex_flags=re.DOTALL),
inputSchema={
@ -236,6 +238,8 @@ class TestTools:
)
assert tools[1].model_dump() == dict(
name="update_user_name_users",
meta=None,
title=None,
annotations=None,
description=IsStr(
regex=r"^Update a user's name\..*$", regex_flags=re.DOTALL

View file

@ -1,3 +1,5 @@
import json
import pytest
from mcp.types import (
AudioContent,
@ -696,7 +698,7 @@ class TestConvertResultToContent:
assert len(result) == 1
assert isinstance(result[0], TextContent)
# Should fall back to default serializer (pydantic_core.to_json)
assert result[0].text == '{\n "a": 1\n}'
assert json.loads(result[0].text) == {"a": 1}
assert "Error serializing tool result" in caplog.text
def test_process_as_single_item_flag(self):
@ -714,7 +716,7 @@ class TestConvertResultToContent:
assert len(result) == 1
assert isinstance(result[0], TextContent)
assert (
result[0].text
== '[\n 1,\n {\n "type": "text",\n "text": "hello",\n "annotations": null\n }\n]'
)
assert json.loads(result[0].text) == [
1,
{"type": "text", "text": "hello", "annotations": None, "_meta": None},
]