Jeremiah Lowin commited on
Commit
ae3ce72
·
1 Parent(s): 120f8eb

Document and test input types

Browse files
docs/servers/tools.mdx CHANGED
@@ -118,9 +118,11 @@ FastMCP supports a wide range of type annotations:
118
  | Collection types | `list[str]`, `dict[str, int]`, `set[int]` | Collections of items - see [Collection Types](#collection-types) |
119
  | Optional types | `float \| None`, `Optional[float]`| Parameters that may be null/omitted - see [Union and Optional Types](#union-and-optional-types) |
120
  | Union types | `str \| int`, `Union[str, int]`| Parameters accepting multiple types - see [Union and Optional Types](#union-and-optional-types) |
121
- | Literal types | `Literal["A", "B"]` | Parameters with specific allowed values - see [Literal Types](#literal-types) |
122
  | Pydantic models | `UserData` | Complex structured data - see [Pydantic Models](#pydantic-models) |
123
 
 
 
124
  #### Optional Arguments
125
 
126
  FastMCP follows Python's standard function parameter conventions. Parameters without default values are required, while those with default values are optional.
@@ -198,6 +200,8 @@ FastMCP automatically converts the value returned by your function into the appr
198
  - **`fastmcp.Image`**: A helper class for easily returning image data. Sent as `ImageContent`.
199
  - **`None`**: Results in an empty response (no content is sent back to the client).
200
 
 
 
201
  ```python
202
  from fastmcp import FastMCP, Image
203
  import io
@@ -328,7 +332,12 @@ The duplicate behavior options are:
328
 
329
  ## Parameter Types
330
 
331
- FastMCP supports a wide variety of parameter types to give you flexibility when designing your tools. When clients send parameters, FastMCP will attempt to coerce values into the appropriate type when possible (for example, parsing JSON strings into structured types).
 
 
 
 
 
332
 
333
  ### Built-in Types
334
 
@@ -350,21 +359,28 @@ These types provide clear expectations to the LLM about what values are acceptab
350
 
351
  ### Collection Types
352
 
353
- For structured data collections, FastMCP supports standard Python collection types:
354
 
355
  ```python
356
  @mcp.tool()
357
  def analyze_data(
358
  values: list[float], # List of numbers
359
- labels: list[str], # List of strings
360
  properties: dict[str, str], # Dictionary with string keys and values
 
 
361
  mixed_data: dict[str, list[int]] # Nested collections
362
  ):
363
  """Analyze collections of data."""
364
  # Implementation...
365
  ```
366
 
367
- Collection types can be nested and combined to represent complex data structures. If a client sends a JSON string like `"[1.5, 2.5, 3.5]"` for a `list[float]` parameter, FastMCP will automatically parse and convert it.
 
 
 
 
 
 
368
 
369
  ### Union and Optional Types
370
 
@@ -383,9 +399,13 @@ def flexible_search(
383
 
384
  Modern Python syntax (`str | int`) is preferred over older `Union[str, int]` forms. Similarly, `str | None` is preferred over `Optional[str]`.
385
 
386
- ### Literal Types
 
 
 
 
387
 
388
- When a parameter must be one of a predefined set of values:
389
 
390
  ```python
391
  from typing import Literal
@@ -396,17 +416,49 @@ def sort_data(
396
  order: Literal["ascending", "descending"] = "ascending",
397
  algorithm: Literal["quicksort", "mergesort", "heapsort"] = "quicksort"
398
  ):
399
- """Sort data using specified order and algorithm."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
  # Implementation...
 
401
  ```
402
 
403
- Literal types help LLMs understand exactly which values are acceptable and provide validation for incoming parameters.
 
 
 
 
404
 
405
- ### Binary Data Handling
406
 
407
  There are two approaches to handling binary data in tool parameters:
408
 
409
- #### Using bytes type
410
 
411
  ```python
412
  @mcp.tool()
@@ -427,7 +479,7 @@ When you annotate a parameter as `bytes`, FastMCP will:
427
 
428
  FastMCP does not automatically decode base64-encoded strings for bytes parameters. If you need to accept base64-encoded data, you should handle the decoding manually as shown below.
429
 
430
- #### Using base64-encoded strings
431
 
432
  ```python
433
  from typing import Annotated
@@ -481,4 +533,67 @@ Using Pydantic models provides:
481
  Clients can provide data for Pydantic model parameters as either:
482
  - A JSON object (string)
483
  - A dictionary with the appropriate structure
484
- - Nested parameters in the appropriate format
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  | Collection types | `list[str]`, `dict[str, int]`, `set[int]` | Collections of items - see [Collection Types](#collection-types) |
119
  | Optional types | `float \| None`, `Optional[float]`| Parameters that may be null/omitted - see [Union and Optional Types](#union-and-optional-types) |
120
  | Union types | `str \| int`, `Union[str, int]`| Parameters accepting multiple types - see [Union and Optional Types](#union-and-optional-types) |
121
+ | Constrained types | `Literal["A", "B"]`, `Enum` | Parameters with specific allowed values - see [Constrained Types](#constrained-types) |
122
  | Pydantic models | `UserData` | Complex structured data - see [Pydantic Models](#pydantic-models) |
123
 
124
+ For additional type annotations not listed here, see the [Parameter Types](#parameter-types) section below for more detailed information and examples.
125
+
126
  #### Optional Arguments
127
 
128
  FastMCP follows Python's standard function parameter conventions. Parameters without default values are required, while those with default values are optional.
 
200
  - **`fastmcp.Image`**: A helper class for easily returning image data. Sent as `ImageContent`.
201
  - **`None`**: Results in an empty response (no content is sent back to the client).
202
 
203
+ FastMCP will attempt to serialize other types to a string if possible.
204
+
205
  ```python
206
  from fastmcp import FastMCP, Image
207
  import io
 
332
 
333
  ## Parameter Types
334
 
335
+ FastMCP supports a wide variety of parameter types to give you flexibility when designing your tools.
336
+
337
+
338
+
339
+ FastMCP supports **type coercion** when possible. This means that if a client sends data that doesn't match the expected type, FastMCP will attempt to convert it to the appropriate type. For example, if a client sends a string for a parameter annotated as `int`, FastMCP will attempt to convert it to an integer. If the conversion is not possible, FastMCP will return a validation error.
340
+
341
 
342
  ### Built-in Types
343
 
 
359
 
360
  ### Collection Types
361
 
362
+ FastMCP supports all standard Python collection types:
363
 
364
  ```python
365
  @mcp.tool()
366
  def analyze_data(
367
  values: list[float], # List of numbers
 
368
  properties: dict[str, str], # Dictionary with string keys and values
369
+ unique_ids: set[int], # Set of unique integers
370
+ coordinates: tuple[float, float], # Tuple with fixed structure
371
  mixed_data: dict[str, list[int]] # Nested collections
372
  ):
373
  """Analyze collections of data."""
374
  # Implementation...
375
  ```
376
 
377
+ All collection types can be used as parameter annotations:
378
+ - `list[T]` - Ordered sequence of items
379
+ - `dict[K, V]` - Key-value mapping
380
+ - `set[T]` - Unordered collection of unique items
381
+ - `tuple[T1, T2, ...]` - Fixed-length sequence with potentially different types
382
+
383
+ Collection types can be nested and combined to represent complex data structures. JSON strings that match the expected structure will be automatically parsed and converted to the appropriate Python collection type.
384
 
385
  ### Union and Optional Types
386
 
 
399
 
400
  Modern Python syntax (`str | int`) is preferred over older `Union[str, int]` forms. Similarly, `str | None` is preferred over `Optional[str]`.
401
 
402
+ ### Constrained Types
403
+
404
+ When a parameter must be one of a predefined set of values, you can use either Literal types or Enums:
405
+
406
+ #### Literals
407
 
408
+ Literals constrain parameters to a specific set of values:
409
 
410
  ```python
411
  from typing import Literal
 
416
  order: Literal["ascending", "descending"] = "ascending",
417
  algorithm: Literal["quicksort", "mergesort", "heapsort"] = "quicksort"
418
  ):
419
+ """Sort data using specific options."""
420
+ # Implementation...
421
+ ```
422
+
423
+ Literal types:
424
+ - Specify exact allowable values directly in the type annotation
425
+ - Help LLMs understand exactly which values are acceptable
426
+ - Provide input validation (errors for invalid values)
427
+ - Create clear schemas for clients
428
+
429
+ #### Enums
430
+
431
+ For more structured sets of constrained values, use Python's Enum class:
432
+
433
+ ```python
434
+ from enum import Enum
435
+
436
+ class Color(Enum):
437
+ RED = "red"
438
+ GREEN = "green"
439
+ BLUE = "blue"
440
+
441
+ @mcp.tool()
442
+ def process_image(
443
+ image_path: str,
444
+ color_filter: Color = Color.RED
445
+ ):
446
+ """Process an image with a color filter."""
447
  # Implementation...
448
+ # color_filter will be a Color enum member
449
  ```
450
 
451
+ When using Enum types:
452
+ - Clients should provide the enum's value (e.g., "red"), not the enum member name (e.g., "RED")
453
+ - FastMCP automatically coerces the string value into the appropriate Enum object
454
+ - Your function receives the actual Enum member (e.g., `Color.RED`)
455
+ - Validation errors are raised for values not in the enum
456
 
457
+ ### Binary Data
458
 
459
  There are two approaches to handling binary data in tool parameters:
460
 
461
+ #### Bytes
462
 
463
  ```python
464
  @mcp.tool()
 
479
 
480
  FastMCP does not automatically decode base64-encoded strings for bytes parameters. If you need to accept base64-encoded data, you should handle the decoding manually as shown below.
481
 
482
+ #### Base64-encoded strings
483
 
484
  ```python
485
  from typing import Annotated
 
533
  Clients can provide data for Pydantic model parameters as either:
534
  - A JSON object (string)
535
  - A dictionary with the appropriate structure
536
+ - Nested parameters in the appropriate format
537
+
538
+ ### Pydantic Fields
539
+
540
+ FastMCP supports robust parameter validation through Pydantic's `Field` class. This is especially useful to ensure that input values meet specific requirements beyond just their type.
541
+
542
+ Note that fields can be used *outside* Pydantic models to provide metadata and validation constraints. The preferred approach is using `Annotated` with `Field`:
543
+
544
+ ```python
545
+ from typing import Annotated
546
+ from pydantic import Field
547
+
548
+ @mcp.tool()
549
+ def analyze_metrics(
550
+ # Numbers with range constraints
551
+ count: Annotated[int, Field(ge=0, le=100)], # 0 <= count <= 100
552
+ ratio: Annotated[float, Field(gt=0, lt=1.0)], # 0 < ratio < 1.0
553
+
554
+ # String with pattern and length constraints
555
+ user_id: Annotated[str, Field(
556
+ pattern=r"^[A-Z]{2}\d{4}$", # Must match regex pattern
557
+ description="User ID in format XX0000"
558
+ )],
559
+
560
+ # String with length constraints
561
+ comment: Annotated[str, Field(min_length=3, max_length=500)] = "",
562
+
563
+ # Numeric constraints
564
+ factor: Annotated[int, Field(multiple_of=5)] = 10, # Must be multiple of 5
565
+ ):
566
+ """Analyze metrics with validated parameters."""
567
+ # Implementation...
568
+ ```
569
+
570
+ You can also use `Field` as a default value, though the `Annotated` approach is preferred:
571
+
572
+ ```python
573
+ @mcp.tool()
574
+ def validate_data(
575
+ # Value constraints
576
+ age: int = Field(ge=0, lt=120), # 0 <= age < 120
577
+
578
+ # String constraints
579
+ email: str = Field(pattern=r"^[\w\.-]+@[\w\.-]+\.\w+$"), # Email pattern
580
+
581
+ # Collection constraints
582
+ tags: list[str] = Field(min_length=1, max_length=10) # 1-10 tags
583
+ ):
584
+ """Process data with field validations."""
585
+ # Implementation...
586
+ ```
587
+
588
+ Common validation options include:
589
+
590
+ | Validation | Type | Description |
591
+ | :--------- | :--- | :---------- |
592
+ | `ge`, `gt` | Number | Greater than (or equal) constraint |
593
+ | `le`, `lt` | Number | Less than (or equal) constraint |
594
+ | `multiple_of` | Number | Value must be a multiple of this number |
595
+ | `min_length`, `max_length` | String, List, etc. | Length constraints |
596
+ | `pattern` | String | Regular expression pattern constraint |
597
+ | `description` | Any | Human-readable description (appears in schema) |
598
+
599
+ When a client sends invalid data, FastMCP will return a validation error explaining why the parameter failed validation.
tests/server/test_server.py CHANGED
@@ -1,8 +1,11 @@
 
 
1
  import pytest
2
  from mcp.types import (
3
  TextContent,
4
  TextResourceContents,
5
  )
 
6
 
7
  from fastmcp import Client, FastMCP
8
  from fastmcp.exceptions import ClientError, NotFoundError
@@ -239,6 +242,36 @@ class TestToolDecorator:
239
  # Original name should not be registered
240
  assert "multiply" not in tools
241
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
  class TestResourceDecorator:
244
  async def test_no_resources_before_decorator(self):
 
1
+ from typing import Annotated
2
+
3
  import pytest
4
  from mcp.types import (
5
  TextContent,
6
  TextResourceContents,
7
  )
8
+ from pydantic import Field
9
 
10
  from fastmcp import Client, FastMCP
11
  from fastmcp.exceptions import ClientError, NotFoundError
 
242
  # Original name should not be registered
243
  assert "multiply" not in tools
244
 
245
+ async def test_tool_with_annotated_arguments(self):
246
+ """Test that tools with annotated arguments work correctly."""
247
+ mcp = FastMCP()
248
+
249
+ @mcp.tool()
250
+ def add(
251
+ x: Annotated[int, Field(description="x is an int")],
252
+ y: Annotated[str, Field(description="y is not an int")],
253
+ ) -> None:
254
+ pass
255
+
256
+ tool = (await mcp.get_tools())["add"]
257
+ assert tool.parameters["properties"]["x"]["description"] == "x is an int"
258
+ assert tool.parameters["properties"]["y"]["description"] == "y is not an int"
259
+
260
+ async def test_tool_with_field_defaults(self):
261
+ """Test that tools with annotated arguments work correctly."""
262
+ mcp = FastMCP()
263
+
264
+ @mcp.tool()
265
+ def add(
266
+ x: int = Field(description="x is an int"),
267
+ y: str = Field(description="y is not an int"),
268
+ ) -> None:
269
+ pass
270
+
271
+ tool = (await mcp.get_tools())["add"]
272
+ assert tool.parameters["properties"]["x"]["description"] == "x is an int"
273
+ assert tool.parameters["properties"]["y"]["description"] == "y is not an int"
274
+
275
 
276
  class TestResourceDecorator:
277
  async def test_no_resources_before_decorator(self):
tests/server/test_server_interactions.py CHANGED
@@ -1,6 +1,8 @@
1
  import base64
2
  import json
 
3
  from pathlib import Path
 
4
 
5
  import pytest
6
  from mcp.types import (
@@ -157,7 +159,32 @@ class TestTools:
157
  assert isinstance(content3, TextContent)
158
  assert content3.text == "direct content"
159
 
160
- async def test_parameter_descriptions(self):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  mcp = FastMCP("Test Server")
162
 
163
  @mcp.tool()
@@ -179,6 +206,8 @@ class TestTools:
179
  assert properties["name"]["description"] == "The name to greet"
180
  assert "title" in properties
181
  assert properties["title"]["description"] == "Optional title"
 
 
182
 
183
  async def test_tool_with_bytes_input(self):
184
  mcp = FastMCP()
@@ -209,6 +238,229 @@ class TestTools:
209
  ):
210
  await client.call_tool("my_tool", {"x": "not an int"})
211
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
 
213
  class TestResources:
214
  async def test_text_resource(self):
 
1
  import base64
2
  import json
3
+ from enum import Enum
4
  from pathlib import Path
5
+ from typing import Annotated, Literal
6
 
7
  import pytest
8
  from mcp.types import (
 
159
  assert isinstance(content3, TextContent)
160
  assert content3.text == "direct content"
161
 
162
+ async def test_parameter_descriptions_with_field_annotations(self):
163
+ mcp = FastMCP("Test Server")
164
+
165
+ @mcp.tool()
166
+ def greet(
167
+ name: Annotated[str, Field(description="The name to greet")],
168
+ title: Annotated[str, Field(description="Optional title", default="")],
169
+ ) -> str:
170
+ """A greeting tool"""
171
+ return f"Hello {title} {name}"
172
+
173
+ async with Client(mcp) as client:
174
+ tools = await client.list_tools()
175
+ assert len(tools) == 1
176
+ tool = tools[0]
177
+
178
+ # Check that parameter descriptions are present in the schema
179
+ properties = tool.inputSchema["properties"]
180
+ assert "name" in properties
181
+ assert properties["name"]["description"] == "The name to greet"
182
+ assert "title" in properties
183
+ assert properties["title"]["description"] == "Optional title"
184
+ assert properties["title"]["default"] == ""
185
+ assert tool.inputSchema["required"] == ["name"]
186
+
187
+ async def test_parameter_descriptions_with_field_defaults(self):
188
  mcp = FastMCP("Test Server")
189
 
190
  @mcp.tool()
 
206
  assert properties["name"]["description"] == "The name to greet"
207
  assert "title" in properties
208
  assert properties["title"]["description"] == "Optional title"
209
+ assert properties["title"]["default"] == ""
210
+ assert tool.inputSchema["required"] == ["name"]
211
 
212
  async def test_tool_with_bytes_input(self):
213
  mcp = FastMCP()
 
238
  ):
239
  await client.call_tool("my_tool", {"x": "not an int"})
240
 
241
+ async def test_tool_int_coercion(self):
242
+ """Test string-to-int type coercion."""
243
+ mcp = FastMCP()
244
+
245
+ @mcp.tool()
246
+ def add_one(x: int) -> int:
247
+ return x + 1
248
+
249
+ async with Client(mcp) as client:
250
+ # String with integer value should be coerced to int
251
+ result = await client.call_tool("add_one", {"x": "42"})
252
+ assert isinstance(result[0], TextContent)
253
+ assert result[0].text == "43"
254
+
255
+ async def test_tool_bool_coercion(self):
256
+ """Test string-to-bool type coercion."""
257
+ mcp = FastMCP()
258
+
259
+ @mcp.tool()
260
+ def toggle(flag: bool) -> bool:
261
+ return not flag
262
+
263
+ async with Client(mcp) as client:
264
+ # String with boolean value should be coerced to bool
265
+ result = await client.call_tool("toggle", {"flag": "true"})
266
+ assert isinstance(result[0], TextContent)
267
+ assert result[0].text == "false"
268
+
269
+ result = await client.call_tool("toggle", {"flag": "false"})
270
+ assert isinstance(result[0], TextContent)
271
+ assert result[0].text == "true"
272
+
273
+ async def test_tool_list_coercion(self):
274
+ """Test JSON string to collection type coercion."""
275
+ mcp = FastMCP()
276
+
277
+ @mcp.tool()
278
+ def process_list(items: list[int]) -> int:
279
+ return sum(items)
280
+
281
+ async with Client(mcp) as client:
282
+ # JSON array string should be coerced to list
283
+ result = await client.call_tool(
284
+ "process_list", {"items": "[1, 2, 3, 4, 5]"}
285
+ )
286
+ assert isinstance(result[0], TextContent)
287
+ assert result[0].text == "15"
288
+
289
+ async def test_tool_list_coercion_error(self):
290
+ """Test that a list coercion error is raised if the input is not a valid list."""
291
+ mcp = FastMCP()
292
+
293
+ @mcp.tool()
294
+ def process_list(items: list[int]) -> int:
295
+ return sum(items)
296
+
297
+ async with Client(mcp) as client:
298
+ with pytest.raises(
299
+ ClientError,
300
+ match="Input should be a valid list",
301
+ ):
302
+ await client.call_tool("process_list", {"items": "['a', 'b', 3]"})
303
+
304
+ async def test_tool_dict_coercion(self):
305
+ """Test JSON string to dict type coercion."""
306
+ mcp = FastMCP()
307
+
308
+ @mcp.tool()
309
+ def process_dict(data: dict[str, int]) -> int:
310
+ return sum(data.values())
311
+
312
+ async with Client(mcp) as client:
313
+ # JSON object string should be coerced to dict
314
+ result = await client.call_tool(
315
+ "process_dict", {"data": '{"a": 1, "b": "2", "c": 3}'}
316
+ )
317
+ assert isinstance(result[0], TextContent)
318
+ assert result[0].text == "6"
319
+
320
+ async def test_tool_set_coercion(self):
321
+ """Test JSON string to set type coercion."""
322
+ mcp = FastMCP()
323
+
324
+ @mcp.tool()
325
+ def process_set(items: set[int]) -> int:
326
+ assert isinstance(items, set)
327
+ return sum(items)
328
+
329
+ async with Client(mcp) as client:
330
+ result = await client.call_tool("process_set", {"items": "[1, 2, 3, 4, 5]"})
331
+ assert isinstance(result[0], TextContent)
332
+ assert result[0].text == "15"
333
+
334
+ async def test_tool_tuple_coercion(self):
335
+ """Test JSON string to tuple type coercion."""
336
+ mcp = FastMCP()
337
+
338
+ @mcp.tool()
339
+ def process_tuple(items: tuple[int, str]) -> int:
340
+ assert isinstance(items, tuple)
341
+ return items[0] + len(items[1])
342
+
343
+ async with Client(mcp) as client:
344
+ result = await client.call_tool("process_tuple", {"items": '["1", "two"]'})
345
+ assert isinstance(result[0], TextContent)
346
+ assert result[0].text == "4"
347
+
348
+ async def test_annotated_field_validation(self):
349
+ mcp = FastMCP()
350
+
351
+ @mcp.tool()
352
+ def analyze(x: Annotated[int, Field(ge=1)]) -> None:
353
+ pass
354
+
355
+ async with Client(mcp) as client:
356
+ with pytest.raises(
357
+ ClientError,
358
+ match="Input should be greater than or equal to 1",
359
+ ):
360
+ await client.call_tool("analyze", {"x": 0})
361
+
362
+ async def test_default_field_validation(self):
363
+ mcp = FastMCP()
364
+
365
+ @mcp.tool()
366
+ def analyze(x: int = Field(ge=1)) -> None:
367
+ pass
368
+
369
+ async with Client(mcp) as client:
370
+ with pytest.raises(
371
+ ClientError,
372
+ match="Input should be greater than or equal to 1",
373
+ ):
374
+ await client.call_tool("analyze", {"x": 0})
375
+
376
+ async def test_default_field_is_still_required_if_no_default_specified(self):
377
+ mcp = FastMCP()
378
+
379
+ @mcp.tool()
380
+ def analyze(x: int = Field()) -> None:
381
+ pass
382
+
383
+ async with Client(mcp) as client:
384
+ with pytest.raises(ClientError, match="Field required"):
385
+ await client.call_tool("analyze", {})
386
+
387
+ async def test_literal_type_validation_error(self):
388
+ mcp = FastMCP()
389
+
390
+ @mcp.tool()
391
+ def analyze(x: Literal["a", "b"]) -> None:
392
+ pass
393
+
394
+ async with Client(mcp) as client:
395
+ with pytest.raises(ClientError, match="Input should be 'a' or 'b'"):
396
+ await client.call_tool("analyze", {"x": "c"})
397
+
398
+ async def test_literal_type_validation_success(self):
399
+ mcp = FastMCP()
400
+
401
+ @mcp.tool()
402
+ def analyze(x: Literal["a", "b"]) -> str:
403
+ return x
404
+
405
+ async with Client(mcp) as client:
406
+ result = await client.call_tool("analyze", {"x": "a"})
407
+ assert isinstance(result[0], TextContent)
408
+ assert result[0].text == "a"
409
+
410
+ async def test_enum_type_validation_error(self):
411
+ mcp = FastMCP()
412
+
413
+ class MyEnum(Enum):
414
+ RED = "red"
415
+ GREEN = "green"
416
+ BLUE = "blue"
417
+
418
+ @mcp.tool()
419
+ def analyze(x: MyEnum) -> str:
420
+ return x.value
421
+
422
+ async with Client(mcp) as client:
423
+ with pytest.raises(
424
+ ClientError, match="Input should be 'red', 'green' or 'blue'"
425
+ ):
426
+ await client.call_tool("analyze", {"x": "some-color"})
427
+
428
+ async def test_enum_type_validation_success(self):
429
+ mcp = FastMCP()
430
+
431
+ class MyEnum(Enum):
432
+ RED = "red"
433
+ GREEN = "green"
434
+ BLUE = "blue"
435
+
436
+ @mcp.tool()
437
+ def analyze(x: MyEnum) -> str:
438
+ return x.value
439
+
440
+ async with Client(mcp) as client:
441
+ result = await client.call_tool("analyze", {"x": "red"})
442
+ assert isinstance(result[0], TextContent)
443
+ assert result[0].text == "red"
444
+
445
+ async def test_union_type_validation(self):
446
+ mcp = FastMCP()
447
+
448
+ @mcp.tool()
449
+ def analyze(x: int | float) -> str:
450
+ return str(x)
451
+
452
+ async with Client(mcp) as client:
453
+ result = await client.call_tool("analyze", {"x": 1})
454
+ assert isinstance(result[0], TextContent)
455
+ assert result[0].text == "1"
456
+
457
+ result = await client.call_tool("analyze", {"x": 1.0})
458
+ assert isinstance(result[0], TextContent)
459
+ assert result[0].text == "1.0"
460
+
461
+ with pytest.raises(ClientError, match="2 validation errors for analyze"):
462
+ await client.call_tool("analyze", {"x": "not a number"})
463
+
464
 
465
  class TestResources:
466
  async def test_text_resource(self):