diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index d7f921d0f..143d664f7 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,4 +1,4 @@
-fail_fast: true
+fail_fast: false
repos:
- repo: https://github.com/abravalheri/validate-pyproject
diff --git a/docs/clients/client.mdx b/docs/clients/client.mdx
index 3c4db65aa..2c097724f 100644
--- a/docs/clients/client.mdx
+++ b/docs/clients/client.mdx
@@ -234,6 +234,30 @@ The standard client methods return user-friendly representations that may change
* **`list_prompts()`**: Retrieves available prompt templates.
* **`get_prompt(name: str, arguments: dict[str, Any] | None = None)`**: Retrieves a rendered prompt message list.
+
+
+**Automatic Argument Serialization**: When calling prompts with complex arguments, the FastMCP client automatically serializes non-string values to JSON strings as required by the MCP specification. This allows you to pass typed objects directly while maintaining protocol compliance.
+
+```python
+from dataclasses import dataclass
+
+@dataclass
+class UserData:
+ name: str
+ age: int
+
+async with client:
+ # You can pass complex objects directly
+ result = await client.get_prompt("analyze_user", {
+ "user": UserData(name="Alice", age=30), # Automatically serialized to JSON
+ "preferences": {"theme": "dark"}, # Dict serialized to JSON string
+ "scores": [85, 92, 78], # List serialized to JSON string
+ "simple_name": "Bob" # Strings passed through unchanged
+ })
+```
+
+The client handles the serialization automatically using `pydantic_core.to_json()` for consistent formatting, while the server can deserialize these JSON strings back to the expected types if using FastMCP's server-side type conversion.
+
### Raw MCP Protocol Objects
diff --git a/src/fastmcp/client/client.py b/src/fastmcp/client/client.py
index 26b14586b..203fcea14 100644
--- a/src/fastmcp/client/client.py
+++ b/src/fastmcp/client/client.py
@@ -7,6 +7,7 @@ from typing import Any, Generic, Literal, cast, overload
import anyio
import httpx
import mcp.types
+import pydantic_core
from exceptiongroup import catch
from mcp import ClientSession
from pydantic import AnyUrl
@@ -508,13 +509,13 @@ class Client(Generic[ClientTransportT]):
# --- Prompt ---
async def get_prompt_mcp(
- self, name: str, arguments: dict[str, str] | None = None
+ self, name: str, arguments: dict[str, Any] | None = None
) -> mcp.types.GetPromptResult:
"""Send a prompts/get request and return the complete MCP protocol result.
Args:
name (str): The name of the prompt to retrieve.
- arguments (dict[str, str] | None, optional): Arguments to pass to the prompt. Defaults to None.
+ arguments (dict[str, Any] | None, optional): Arguments to pass to the prompt. Defaults to None.
Returns:
mcp.types.GetPromptResult: The complete response object from the protocol,
@@ -523,17 +524,32 @@ class Client(Generic[ClientTransportT]):
Raises:
RuntimeError: If called while the client is not connected.
"""
- result = await self.session.get_prompt(name=name, arguments=arguments)
+ # Serialize arguments for MCP protocol - convert non-string values to JSON
+ serialized_arguments: dict[str, str] | None = None
+ if arguments:
+ serialized_arguments = {}
+ for key, value in arguments.items():
+ if isinstance(value, str):
+ serialized_arguments[key] = value
+ else:
+ # Use pydantic_core.to_json for consistent serialization
+ serialized_arguments[key] = pydantic_core.to_json(value).decode(
+ "utf-8"
+ )
+
+ result = await self.session.get_prompt(
+ name=name, arguments=serialized_arguments
+ )
return result
async def get_prompt(
- self, name: str, arguments: dict[str, str] | None = None
+ self, name: str, arguments: dict[str, Any] | None = None
) -> mcp.types.GetPromptResult:
"""Retrieve a rendered prompt message list from the server.
Args:
name (str): The name of the prompt to retrieve.
- arguments (dict[str, str] | None, optional): Arguments to pass to the prompt. Defaults to None.
+ arguments (dict[str, Any] | None, optional): Arguments to pass to the prompt. Defaults to None.
Returns:
mcp.types.GetPromptResult: The complete response object from the protocol,
diff --git a/tests/client/test_client.py b/tests/client/test_client.py
index f792a15e0..55210c432 100644
--- a/tests/client/test_client.py
+++ b/tests/client/test_client.py
@@ -220,6 +220,101 @@ async def test_get_prompt_mcp(fastmcp_server):
assert result.description == "Example greeting prompt."
+async def test_client_serializes_all_non_string_arguments():
+ """Test that client always serializes non-string arguments to JSON, regardless of server types."""
+ server = FastMCP("TestServer")
+
+ @server.prompt
+ def echo_args(arg1: str, arg2: str, arg3: str) -> str:
+ """Server accepts all string args but client sends mixed types."""
+ return f"arg1: {arg1}, arg2: {arg2}, arg3: {arg3}"
+
+ client = Client(transport=FastMCPTransport(server))
+
+ async with client:
+ result = await client.get_prompt(
+ "echo_args",
+ {
+ "arg1": "hello", # string - should pass through
+ "arg2": [1, 2, 3], # list - should be JSON serialized
+ "arg3": {"key": "value"}, # dict - should be JSON serialized
+ },
+ )
+
+ content = result.messages[0].content.text # type: ignore[attr-defined]
+ assert "arg1: hello" in content
+ assert "arg2: [1,2,3]" in content # JSON serialized list
+ assert 'arg3: {"key":"value"}' in content # JSON serialized dict
+
+
+async def test_client_server_type_conversion_integration():
+ """Test that client serialization works with server-side type conversion."""
+ server = FastMCP("TestServer")
+
+ @server.prompt
+ def typed_prompt(numbers: list[int], config: dict[str, str]) -> str:
+ """Server expects typed args - will convert from JSON strings."""
+ return f"Got {len(numbers)} numbers and {len(config)} config items"
+
+ client = Client(transport=FastMCPTransport(server))
+
+ async with client:
+ result = await client.get_prompt(
+ "typed_prompt",
+ {"numbers": [1, 2, 3, 4], "config": {"theme": "dark", "lang": "en"}},
+ )
+
+ content = result.messages[0].content.text # type: ignore[attr-defined]
+ assert "Got 4 numbers and 2 config items" in content
+
+
+async def test_client_serialization_error():
+ """Test client error when object cannot be serialized."""
+ import pydantic_core
+
+ server = FastMCP("TestServer")
+
+ @server.prompt
+ def any_prompt(data: str) -> str:
+ return f"Got: {data}"
+
+ # Create an unserializable object
+ class UnserializableClass:
+ def __init__(self):
+ self.func = lambda x: x # functions can't be JSON serialized
+
+ client = Client(transport=FastMCPTransport(server))
+
+ async with client:
+ with pytest.raises(
+ pydantic_core.PydanticSerializationError, match="Unable to serialize"
+ ):
+ await client.get_prompt("any_prompt", {"data": UnserializableClass()})
+
+
+async def test_server_deserialization_error():
+ """Test server error when JSON string cannot be converted to expected type."""
+ from mcp import McpError
+
+ server = FastMCP("TestServer")
+
+ @server.prompt
+ def strict_typed_prompt(numbers: list[int]) -> str:
+ """Expects list of integers but will receive invalid JSON."""
+ return f"Got {len(numbers)} numbers"
+
+ client = Client(transport=FastMCPTransport(server))
+
+ async with client:
+ with pytest.raises(McpError, match="Error rendering prompt"):
+ await client.get_prompt(
+ "strict_typed_prompt",
+ {
+ "numbers": "not valid json" # This will fail server-side conversion
+ },
+ )
+
+
async def test_read_resource_invalid_uri(fastmcp_server):
"""Test reading a resource with an invalid URI."""
client = Client(transport=FastMCPTransport(fastmcp_server))