Jeremiah Lowin commited on
Commit
47f188e
·
unverified ·
2 Parent(s): c8453683c4246c

Merge pull request #888 from jlowin/protocol-typing

Browse files
src/fastmcp/client/client.py CHANGED
@@ -9,6 +9,7 @@ import httpx
9
  import mcp.types
10
  from exceptiongroup import catch
11
  from mcp import ClientSession
 
12
  from pydantic import AnyUrl
13
 
14
  import fastmcp
@@ -29,7 +30,6 @@ from fastmcp.exceptions import ToolError
29
  from fastmcp.server import FastMCP
30
  from fastmcp.utilities.exceptions import get_catch_handlers
31
  from fastmcp.utilities.mcp_config import MCPConfig
32
- from fastmcp.utilities.types import MCPContent
33
 
34
  from .transports import (
35
  ClientTransportT,
@@ -659,7 +659,7 @@ class Client(Generic[ClientTransportT]):
659
  arguments: dict[str, Any] | None = None,
660
  timeout: datetime.timedelta | float | int | None = None,
661
  progress_handler: ProgressHandler | None = None,
662
- ) -> list[MCPContent]:
663
  """Call a tool on the server.
664
 
665
  Unlike call_tool_mcp, this method raises a ToolError if the tool call results in an error.
 
9
  import mcp.types
10
  from exceptiongroup import catch
11
  from mcp import ClientSession
12
+ from mcp.types import ContentBlock
13
  from pydantic import AnyUrl
14
 
15
  import fastmcp
 
30
  from fastmcp.server import FastMCP
31
  from fastmcp.utilities.exceptions import get_catch_handlers
32
  from fastmcp.utilities.mcp_config import MCPConfig
 
33
 
34
  from .transports import (
35
  ClientTransportT,
 
659
  arguments: dict[str, Any] | None = None,
660
  timeout: datetime.timedelta | float | int | None = None,
661
  progress_handler: ProgressHandler | None = None,
662
+ ) -> list[ContentBlock]:
663
  """Call a tool on the server.
664
 
665
  Unlike call_tool_mcp, this method raises a ToolError if the tool call results in an error.
src/fastmcp/prompts/prompt.py CHANGED
@@ -8,9 +8,9 @@ from collections.abc import Awaitable, Callable, Sequence
8
  from typing import TYPE_CHECKING, Any
9
 
10
  import pydantic_core
 
11
  from mcp.types import Prompt as MCPPrompt
12
  from mcp.types import PromptArgument as MCPPromptArgument
13
- from mcp.types import PromptMessage, Role, TextContent
14
  from pydantic import Field, TypeAdapter, validate_call
15
 
16
  from fastmcp.exceptions import PromptError
@@ -20,7 +20,6 @@ from fastmcp.utilities.json_schema import compress_schema
20
  from fastmcp.utilities.logging import get_logger
21
  from fastmcp.utilities.types import (
22
  FastMCPBaseModel,
23
- MCPContent,
24
  find_kwarg_by_type,
25
  get_cached_typeadapter,
26
  )
@@ -33,7 +32,7 @@ logger = get_logger(__name__)
33
 
34
 
35
  def Message(
36
- content: str | MCPContent, role: Role | None = None, **kwargs: Any
37
  ) -> PromptMessage:
38
  """A user-friendly constructor for PromptMessage."""
39
  if isinstance(content, str):
 
8
  from typing import TYPE_CHECKING, Any
9
 
10
  import pydantic_core
11
+ from mcp.types import ContentBlock, PromptMessage, Role, TextContent
12
  from mcp.types import Prompt as MCPPrompt
13
  from mcp.types import PromptArgument as MCPPromptArgument
 
14
  from pydantic import Field, TypeAdapter, validate_call
15
 
16
  from fastmcp.exceptions import PromptError
 
20
  from fastmcp.utilities.logging import get_logger
21
  from fastmcp.utilities.types import (
22
  FastMCPBaseModel,
 
23
  find_kwarg_by_type,
24
  get_cached_typeadapter,
25
  )
 
32
 
33
 
34
  def Message(
35
+ content: str | ContentBlock, role: Role | None = None, **kwargs: Any
36
  ) -> PromptMessage:
37
  """A user-friendly constructor for PromptMessage."""
38
  if isinstance(content, str):
src/fastmcp/server/context.py CHANGED
@@ -11,6 +11,7 @@ from mcp.server.lowlevel.helper_types import ReadResourceContents
11
  from mcp.server.lowlevel.server import request_ctx
12
  from mcp.shared.context import RequestContext
13
  from mcp.types import (
 
14
  CreateMessageResult,
15
  ModelHint,
16
  ModelPreferences,
@@ -25,7 +26,6 @@ import fastmcp.server.dependencies
25
  from fastmcp import settings
26
  from fastmcp.server.server import FastMCP
27
  from fastmcp.utilities.logging import get_logger
28
- from fastmcp.utilities.types import MCPContent
29
 
30
  logger = get_logger(__name__)
31
 
@@ -243,7 +243,7 @@ class Context:
243
  temperature: float | None = None,
244
  max_tokens: int | None = None,
245
  model_preferences: ModelPreferences | str | list[str] | None = None,
246
- ) -> MCPContent:
247
  """
248
  Send a sampling request to the client and await the response.
249
 
 
11
  from mcp.server.lowlevel.server import request_ctx
12
  from mcp.shared.context import RequestContext
13
  from mcp.types import (
14
+ ContentBlock,
15
  CreateMessageResult,
16
  ModelHint,
17
  ModelPreferences,
 
26
  from fastmcp import settings
27
  from fastmcp.server.server import FastMCP
28
  from fastmcp.utilities.logging import get_logger
 
29
 
30
  logger = get_logger(__name__)
31
 
 
243
  temperature: float | None = None,
244
  max_tokens: int | None = None,
245
  model_preferences: ModelPreferences | str | list[str] | None = None,
246
+ ) -> ContentBlock:
247
  """
248
  Send a sampling request to the client and await the response.
249
 
src/fastmcp/server/openapi.py CHANGED
@@ -13,7 +13,7 @@ from re import Pattern
13
  from typing import TYPE_CHECKING, Any, Literal
14
 
15
  import httpx
16
- from mcp.types import ToolAnnotations
17
  from pydantic.networks import AnyUrl
18
 
19
  import fastmcp
@@ -29,7 +29,6 @@ from fastmcp.utilities.openapi import (
29
  _combine_schemas,
30
  format_description_with_responses,
31
  )
32
- from fastmcp.utilities.types import MCPContent
33
 
34
  if TYPE_CHECKING:
35
  from fastmcp.server import Context
@@ -255,7 +254,7 @@ class OpenAPITool(Tool):
255
  """Custom representation to prevent recursion errors when printing."""
256
  return f"OpenAPITool(name={self.name!r}, method={self._route.method}, path={self._route.path})"
257
 
258
- async def run(self, arguments: dict[str, Any]) -> list[MCPContent]:
259
  """Execute the HTTP request based on the route configuration."""
260
 
261
  # Prepare URL
 
13
  from typing import TYPE_CHECKING, Any, Literal
14
 
15
  import httpx
16
+ from mcp.types import ContentBlock, ToolAnnotations
17
  from pydantic.networks import AnyUrl
18
 
19
  import fastmcp
 
29
  _combine_schemas,
30
  format_description_with_responses,
31
  )
 
32
 
33
  if TYPE_CHECKING:
34
  from fastmcp.server import Context
 
254
  """Custom representation to prevent recursion errors when printing."""
255
  return f"OpenAPITool(name={self.name!r}, method={self._route.method}, path={self._route.path})"
256
 
257
+ async def run(self, arguments: dict[str, Any]) -> list[ContentBlock]:
258
  """Execute the HTTP request based on the route configuration."""
259
 
260
  # Prepare URL
src/fastmcp/server/proxy.py CHANGED
@@ -8,6 +8,7 @@ from mcp.shared.exceptions import McpError
8
  from mcp.types import (
9
  METHOD_NOT_FOUND,
10
  BlobResourceContents,
 
11
  GetPromptResult,
12
  TextResourceContents,
13
  )
@@ -25,7 +26,6 @@ from fastmcp.server.server import FastMCP
25
  from fastmcp.tools.tool import Tool
26
  from fastmcp.tools.tool_manager import ToolManager
27
  from fastmcp.utilities.logging import get_logger
28
- from fastmcp.utilities.types import MCPContent
29
 
30
  if TYPE_CHECKING:
31
  from fastmcp.server import Context
@@ -67,7 +67,9 @@ class ProxyToolManager(ToolManager):
67
  tools_dict = await self.get_tools()
68
  return list(tools_dict.values())
69
 
70
- async def call_tool(self, key: str, arguments: dict[str, Any]) -> list[MCPContent]:
 
 
71
  """Calls a tool, trying local/mounted first, then proxy if not found."""
72
  try:
73
  # First try local and mounted tools
@@ -230,7 +232,7 @@ class ProxyTool(Tool):
230
  self,
231
  arguments: dict[str, Any],
232
  context: Context | None = None,
233
- ) -> list[MCPContent]:
234
  """Executes the tool by making a call through the client."""
235
  # This is where the remote execution logic lives.
236
  async with self._client:
 
8
  from mcp.types import (
9
  METHOD_NOT_FOUND,
10
  BlobResourceContents,
11
+ ContentBlock,
12
  GetPromptResult,
13
  TextResourceContents,
14
  )
 
26
  from fastmcp.tools.tool import Tool
27
  from fastmcp.tools.tool_manager import ToolManager
28
  from fastmcp.utilities.logging import get_logger
 
29
 
30
  if TYPE_CHECKING:
31
  from fastmcp.server import Context
 
67
  tools_dict = await self.get_tools()
68
  return list(tools_dict.values())
69
 
70
+ async def call_tool(
71
+ self, key: str, arguments: dict[str, Any]
72
+ ) -> list[ContentBlock]:
73
  """Calls a tool, trying local/mounted first, then proxy if not found."""
74
  try:
75
  # First try local and mounted tools
 
232
  self,
233
  arguments: dict[str, Any],
234
  context: Context | None = None,
235
+ ) -> list[ContentBlock]:
236
  """Executes the tool by making a call through the client."""
237
  # This is where the remote execution logic lives.
238
  async with self._client:
src/fastmcp/server/server.py CHANGED
@@ -27,6 +27,7 @@ from mcp.server.lowlevel.server import Server as MCPServer
27
  from mcp.server.stdio import stdio_server
28
  from mcp.types import (
29
  AnyFunction,
 
30
  GetPromptResult,
31
  ToolAnnotations,
32
  )
@@ -62,7 +63,6 @@ from fastmcp.utilities.cache import TimedCache
62
  from fastmcp.utilities.components import FastMCPComponent
63
  from fastmcp.utilities.logging import get_logger
64
  from fastmcp.utilities.mcp_config import MCPConfig
65
- from fastmcp.utilities.types import MCPContent
66
 
67
  if TYPE_CHECKING:
68
  from fastmcp.client import Client
@@ -586,7 +586,7 @@ class FastMCP(Generic[LifespanResultT]):
586
 
587
  async def _mcp_call_tool(
588
  self, key: str, arguments: dict[str, Any]
589
- ) -> list[MCPContent]:
590
  """
591
  Handle MCP 'callTool' requests.
592
 
@@ -609,14 +609,16 @@ class FastMCP(Generic[LifespanResultT]):
609
  except NotFoundError:
610
  raise NotFoundError(f"Unknown tool: {key}")
611
 
612
- async def _call_tool(self, key: str, arguments: dict[str, Any]) -> list[MCPContent]:
 
 
613
  """
614
  Applies this server's middleware and delegates the filtered call to the manager.
615
  """
616
 
617
  async def _handler(
618
  context: MiddlewareContext[mcp.types.CallToolRequestParams],
619
- ) -> list[MCPContent]:
620
  tool = await self._tool_manager.get_tool(context.message.name)
621
  if not self._should_enable_component(tool):
622
  raise NotFoundError(f"Unknown tool: {context.message.name!r}")
 
27
  from mcp.server.stdio import stdio_server
28
  from mcp.types import (
29
  AnyFunction,
30
+ ContentBlock,
31
  GetPromptResult,
32
  ToolAnnotations,
33
  )
 
63
  from fastmcp.utilities.components import FastMCPComponent
64
  from fastmcp.utilities.logging import get_logger
65
  from fastmcp.utilities.mcp_config import MCPConfig
 
66
 
67
  if TYPE_CHECKING:
68
  from fastmcp.client import Client
 
586
 
587
  async def _mcp_call_tool(
588
  self, key: str, arguments: dict[str, Any]
589
+ ) -> list[ContentBlock]:
590
  """
591
  Handle MCP 'callTool' requests.
592
 
 
609
  except NotFoundError:
610
  raise NotFoundError(f"Unknown tool: {key}")
611
 
612
+ async def _call_tool(
613
+ self, key: str, arguments: dict[str, Any]
614
+ ) -> list[ContentBlock]:
615
  """
616
  Applies this server's middleware and delegates the filtered call to the manager.
617
  """
618
 
619
  async def _handler(
620
  context: MiddlewareContext[mcp.types.CallToolRequestParams],
621
+ ) -> list[ContentBlock]:
622
  tool = await self._tool_manager.get_tool(context.message.name)
623
  if not self._should_enable_component(tool):
624
  raise NotFoundError(f"Unknown tool: {context.message.name!r}")
src/fastmcp/tools/tool.py CHANGED
@@ -7,7 +7,7 @@ from dataclasses import dataclass
7
  from typing import TYPE_CHECKING, Any
8
 
9
  import pydantic_core
10
- from mcp.types import TextContent, ToolAnnotations
11
  from mcp.types import Tool as MCPTool
12
  from pydantic import Field
13
 
@@ -20,7 +20,6 @@ from fastmcp.utilities.types import (
20
  Audio,
21
  File,
22
  Image,
23
- MCPContent,
24
  find_kwarg_by_type,
25
  get_cached_typeadapter,
26
  )
@@ -78,7 +77,7 @@ class Tool(FastMCPComponent):
78
  enabled=enabled,
79
  )
80
 
81
- async def run(self, arguments: dict[str, Any]) -> list[MCPContent]:
82
  """Run the tool with arguments."""
83
  raise NotImplementedError("Subclasses must implement run()")
84
 
@@ -143,7 +142,7 @@ class FunctionTool(Tool):
143
  enabled=enabled if enabled is not None else True,
144
  )
145
 
146
- async def run(self, arguments: dict[str, Any]) -> list[MCPContent]:
147
  """Run the tool with arguments."""
148
  from fastmcp.server.context import Context
149
 
@@ -264,12 +263,12 @@ def _convert_to_content(
264
  result: Any,
265
  serializer: Callable[[Any], str] | None = None,
266
  _process_as_single_item: bool = False,
267
- ) -> list[MCPContent]:
268
  """Convert a result to a sequence of content objects."""
269
  if result is None:
270
  return []
271
 
272
- if isinstance(result, MCPContent):
273
  return [result]
274
 
275
  if isinstance(result, Image):
@@ -292,7 +291,7 @@ def _convert_to_content(
292
  other_content = []
293
 
294
  for item in result:
295
- if isinstance(item, MCPContent | Image | Audio | File):
296
  mcp_types.append(_convert_to_content(item)[0])
297
  else:
298
  other_content.append(item)
 
7
  from typing import TYPE_CHECKING, Any
8
 
9
  import pydantic_core
10
+ from mcp.types import ContentBlock, TextContent, ToolAnnotations
11
  from mcp.types import Tool as MCPTool
12
  from pydantic import Field
13
 
 
20
  Audio,
21
  File,
22
  Image,
 
23
  find_kwarg_by_type,
24
  get_cached_typeadapter,
25
  )
 
77
  enabled=enabled,
78
  )
79
 
80
+ async def run(self, arguments: dict[str, Any]) -> list[ContentBlock]:
81
  """Run the tool with arguments."""
82
  raise NotImplementedError("Subclasses must implement run()")
83
 
 
142
  enabled=enabled if enabled is not None else True,
143
  )
144
 
145
+ async def run(self, arguments: dict[str, Any]) -> list[ContentBlock]:
146
  """Run the tool with arguments."""
147
  from fastmcp.server.context import Context
148
 
 
263
  result: Any,
264
  serializer: Callable[[Any], str] | None = None,
265
  _process_as_single_item: bool = False,
266
+ ) -> list[ContentBlock]:
267
  """Convert a result to a sequence of content objects."""
268
  if result is None:
269
  return []
270
 
271
+ if isinstance(result, ContentBlock):
272
  return [result]
273
 
274
  if isinstance(result, Image):
 
291
  other_content = []
292
 
293
  for item in result:
294
+ if isinstance(item, ContentBlock | Image | Audio | File):
295
  mcp_types.append(_convert_to_content(item)[0])
296
  else:
297
  other_content.append(item)
src/fastmcp/tools/tool_manager.py CHANGED
@@ -4,14 +4,13 @@ import warnings
4
  from collections.abc import Callable
5
  from typing import TYPE_CHECKING, Any
6
 
7
- from mcp.types import ToolAnnotations
8
 
9
  from fastmcp import settings
10
  from fastmcp.exceptions import NotFoundError, ToolError
11
  from fastmcp.settings import DuplicateBehavior
12
  from fastmcp.tools.tool import Tool
13
  from fastmcp.utilities.logging import get_logger
14
- from fastmcp.utilities.types import MCPContent
15
 
16
  if TYPE_CHECKING:
17
  from fastmcp.server.server import MountedServer
@@ -170,7 +169,9 @@ class ToolManager:
170
  else:
171
  raise NotFoundError(f"Tool {key!r} not found")
172
 
173
- async def call_tool(self, key: str, arguments: dict[str, Any]) -> list[MCPContent]:
 
 
174
  """
175
  Internal API for servers: Finds and calls a tool, respecting the
176
  filtered protocol path.
 
4
  from collections.abc import Callable
5
  from typing import TYPE_CHECKING, Any
6
 
7
+ from mcp.types import ContentBlock, ToolAnnotations
8
 
9
  from fastmcp import settings
10
  from fastmcp.exceptions import NotFoundError, ToolError
11
  from fastmcp.settings import DuplicateBehavior
12
  from fastmcp.tools.tool import Tool
13
  from fastmcp.utilities.logging import get_logger
 
14
 
15
  if TYPE_CHECKING:
16
  from fastmcp.server.server import MountedServer
 
169
  else:
170
  raise NotFoundError(f"Tool {key!r} not found")
171
 
172
+ async def call_tool(
173
+ self, key: str, arguments: dict[str, Any]
174
+ ) -> list[ContentBlock]:
175
  """
176
  Internal API for servers: Finds and calls a tool, respecting the
177
  filtered protocol path.
src/fastmcp/tools/tool_transform.py CHANGED
@@ -7,12 +7,12 @@ from dataclasses import dataclass
7
  from types import EllipsisType
8
  from typing import Any, Literal
9
 
10
- from mcp.types import ToolAnnotations
11
  from pydantic import ConfigDict
12
 
13
  from fastmcp.tools.tool import ParsedFunction, Tool
14
  from fastmcp.utilities.logging import get_logger
15
- from fastmcp.utilities.types import MCPContent, get_cached_typeadapter
16
 
17
  logger = get_logger(__name__)
18
 
@@ -202,7 +202,7 @@ class TransformedTool(Tool):
202
  forwarding_fn: Callable[..., Any] # Always present, handles arg transformation
203
  transform_args: dict[str, ArgTransform]
204
 
205
- async def run(self, arguments: dict[str, Any]) -> list[MCPContent]:
206
  """Run the tool with context set for forward() functions.
207
 
208
  This method executes the tool's function while setting up the context
 
7
  from types import EllipsisType
8
  from typing import Any, Literal
9
 
10
+ from mcp.types import ContentBlock, ToolAnnotations
11
  from pydantic import ConfigDict
12
 
13
  from fastmcp.tools.tool import ParsedFunction, Tool
14
  from fastmcp.utilities.logging import get_logger
15
+ from fastmcp.utilities.types import get_cached_typeadapter
16
 
17
  logger = get_logger(__name__)
18
 
 
202
  forwarding_fn: Callable[..., Any] # Always present, handles arg transformation
203
  transform_args: dict[str, ArgTransform]
204
 
205
+ async def run(self, arguments: dict[str, Any]) -> list[ContentBlock]:
206
  """Run the tool with context set for forward() functions.
207
 
208
  This method executes the tool's function while setting up the context
src/fastmcp/utilities/types.py CHANGED
@@ -7,7 +7,7 @@ from collections.abc import Callable
7
  from functools import lru_cache
8
  from pathlib import Path
9
  from types import UnionType
10
- from typing import Annotated, TypeAlias, TypeVar, Union, get_args, get_origin
11
 
12
  from mcp.types import (
13
  Annotations,
@@ -15,15 +15,12 @@ from mcp.types import (
15
  BlobResourceContents,
16
  EmbeddedResource,
17
  ImageContent,
18
- TextContent,
19
- TextResourceContents, # Added import
20
  )
21
  from pydantic import AnyUrl, BaseModel, ConfigDict, TypeAdapter, UrlConstraints
22
 
23
  T = TypeVar("T")
24
 
25
- MCPContent: TypeAlias = TextContent | ImageContent | AudioContent | EmbeddedResource
26
-
27
 
28
  class FastMCPBaseModel(BaseModel):
29
  """Base model for FastMCP models."""
 
7
  from functools import lru_cache
8
  from pathlib import Path
9
  from types import UnionType
10
+ from typing import Annotated, TypeVar, Union, get_args, get_origin
11
 
12
  from mcp.types import (
13
  Annotations,
 
15
  BlobResourceContents,
16
  EmbeddedResource,
17
  ImageContent,
18
+ TextResourceContents,
 
19
  )
20
  from pydantic import AnyUrl, BaseModel, ConfigDict, TypeAdapter, UrlConstraints
21
 
22
  T = TypeVar("T")
23
 
 
 
24
 
25
  class FastMCPBaseModel(BaseModel):
26
  """Base model for FastMCP models."""
tests/server/openapi/test_openapi.py CHANGED
@@ -223,6 +223,8 @@ class TestTools:
223
 
224
  assert tools[0].model_dump() == dict(
225
  name="create_user_users_post",
 
 
226
  annotations=None,
227
  description=IsStr(regex=r"^Create a new user\..*$", regex_flags=re.DOTALL),
228
  inputSchema={
@@ -236,6 +238,8 @@ class TestTools:
236
  )
237
  assert tools[1].model_dump() == dict(
238
  name="update_user_name_users",
 
 
239
  annotations=None,
240
  description=IsStr(
241
  regex=r"^Update a user's name\..*$", regex_flags=re.DOTALL
 
223
 
224
  assert tools[0].model_dump() == dict(
225
  name="create_user_users_post",
226
+ meta=None,
227
+ title=None,
228
  annotations=None,
229
  description=IsStr(regex=r"^Create a new user\..*$", regex_flags=re.DOTALL),
230
  inputSchema={
 
238
  )
239
  assert tools[1].model_dump() == dict(
240
  name="update_user_name_users",
241
+ meta=None,
242
+ title=None,
243
  annotations=None,
244
  description=IsStr(
245
  regex=r"^Update a user's name\..*$", regex_flags=re.DOTALL
tests/tools/test_tool.py CHANGED
@@ -1,3 +1,5 @@
 
 
1
  import pytest
2
  from mcp.types import (
3
  AudioContent,
@@ -696,7 +698,7 @@ class TestConvertResultToContent:
696
  assert len(result) == 1
697
  assert isinstance(result[0], TextContent)
698
  # Should fall back to default serializer (pydantic_core.to_json)
699
- assert result[0].text == '{\n "a": 1\n}'
700
  assert "Error serializing tool result" in caplog.text
701
 
702
  def test_process_as_single_item_flag(self):
@@ -714,7 +716,7 @@ class TestConvertResultToContent:
714
  assert len(result) == 1
715
  assert isinstance(result[0], TextContent)
716
 
717
- assert (
718
- result[0].text
719
- == '[\n 1,\n {\n "type": "text",\n "text": "hello",\n "annotations": null\n }\n]'
720
- )
 
1
+ import json
2
+
3
  import pytest
4
  from mcp.types import (
5
  AudioContent,
 
698
  assert len(result) == 1
699
  assert isinstance(result[0], TextContent)
700
  # Should fall back to default serializer (pydantic_core.to_json)
701
+ assert json.loads(result[0].text) == {"a": 1}
702
  assert "Error serializing tool result" in caplog.text
703
 
704
  def test_process_as_single_item_flag(self):
 
716
  assert len(result) == 1
717
  assert isinstance(result[0], TextContent)
718
 
719
+ assert json.loads(result[0].text) == [
720
+ 1,
721
+ {"type": "text", "text": "hello", "annotations": None, "_meta": None},
722
+ ]