mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-22 21:44:18 +02:00
Merge pull request #888 from jlowin/protocol-typing
This commit is contained in:
commit
e5364787e1
12 changed files with 45 additions and 40 deletions
|
|
@ -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
|
||||
|
|
@ -29,7 +30,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,
|
||||
|
|
@ -659,7 +659,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.
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -11,6 +11,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,
|
||||
|
|
@ -25,7 +26,6 @@ import fastmcp.server.dependencies
|
|||
from fastmcp import settings
|
||||
from fastmcp.server.server import FastMCP
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
from fastmcp.utilities.types import MCPContent
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
|
@ -243,7 +243,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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue