Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
0ce024d
1
Parent(s): ae3ce72
Update docs and tests
Browse files- docs/servers/tools.mdx +66 -4
- tests/server/test_server_interactions.py +143 -0
docs/servers/tools.mdx
CHANGED
|
@@ -109,16 +109,19 @@ Field provides several validation and documentation features:
|
|
| 109 |
|
| 110 |
#### Supported Types
|
| 111 |
|
| 112 |
-
FastMCP supports a wide range of type annotations:
|
| 113 |
|
| 114 |
| Type Annotation | Example | Description |
|
| 115 |
| :---------------------- | :---------------------------- | :---------------------------------- |
|
| 116 |
| Basic types | `int`, `float`, `str`, `bool` | Simple scalar values - see [Built-in Types](#built-in-types) |
|
| 117 |
-
| Binary data | `bytes` | Binary content - see [Binary Data
|
|
|
|
| 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.
|
|
@@ -334,11 +337,10 @@ The duplicate behavior options are:
|
|
| 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 |
|
| 344 |
The most common parameter types are Python's built-in scalar types:
|
|
@@ -357,6 +359,32 @@ def process_values(
|
|
| 357 |
|
| 358 |
These types provide clear expectations to the LLM about what values are acceptable and allow FastMCP to validate inputs properly. Even if a client provides a string like "42", it will be coerced to an integer for parameters annotated as `int`.
|
| 359 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
### Collection Types
|
| 361 |
|
| 362 |
FastMCP supports all standard Python collection types:
|
|
@@ -502,6 +530,40 @@ def process_image_data(
|
|
| 502 |
|
| 503 |
This approach is recommended when you expect to receive base64-encoded binary data from clients.
|
| 504 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 505 |
### Pydantic Models
|
| 506 |
|
| 507 |
For complex, structured data with nested fields and validation, use Pydantic models:
|
|
|
|
| 109 |
|
| 110 |
#### Supported Types
|
| 111 |
|
| 112 |
+
FastMCP supports a wide range of type annotations, including all Pydantic types:
|
| 113 |
|
| 114 |
| Type Annotation | Example | Description |
|
| 115 |
| :---------------------- | :---------------------------- | :---------------------------------- |
|
| 116 |
| Basic types | `int`, `float`, `str`, `bool` | Simple scalar values - see [Built-in Types](#built-in-types) |
|
| 117 |
+
| Binary data | `bytes` | Binary content - see [Binary Data](#binary-data) |
|
| 118 |
+
| Date and Time | `datetime`, `date`, `timedelta` | Date and time objects - see [Date and Time Types](#date-and-time-types) |
|
| 119 |
| Collection types | `list[str]`, `dict[str, int]`, `set[int]` | Collections of items - see [Collection Types](#collection-types) |
|
| 120 |
| Optional types | `float \| None`, `Optional[float]`| Parameters that may be null/omitted - see [Union and Optional Types](#union-and-optional-types) |
|
| 121 |
| Union types | `str \| int`, `Union[str, int]`| Parameters accepting multiple types - see [Union and Optional Types](#union-and-optional-types) |
|
| 122 |
| Constrained types | `Literal["A", "B"]`, `Enum` | Parameters with specific allowed values - see [Constrained Types](#constrained-types) |
|
| 123 |
+
| Paths | `Path` | File system paths - see [Paths](#paths) |
|
| 124 |
+
| UUIDs | `UUID` | Universally unique identifiers - see [UUIDs](#uuids) |
|
| 125 |
| Pydantic models | `UserData` | Complex structured data - see [Pydantic Models](#pydantic-models) |
|
| 126 |
|
| 127 |
For additional type annotations not listed here, see the [Parameter Types](#parameter-types) section below for more detailed information and examples.
|
|
|
|
| 337 |
|
| 338 |
FastMCP supports a wide variety of parameter types to give you flexibility when designing your tools.
|
| 339 |
|
| 340 |
+
FastMCP generally supports all types that Pydantic supports as fields, including all Pydantic custom types. This means you can use any type that can be validated and parsed by Pydantic in your tool parameters.
|
| 341 |
|
| 342 |
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.
|
| 343 |
|
|
|
|
| 344 |
### Built-in Types
|
| 345 |
|
| 346 |
The most common parameter types are Python's built-in scalar types:
|
|
|
|
| 359 |
|
| 360 |
These types provide clear expectations to the LLM about what values are acceptable and allow FastMCP to validate inputs properly. Even if a client provides a string like "42", it will be coerced to an integer for parameters annotated as `int`.
|
| 361 |
|
| 362 |
+
### Date and Time Types
|
| 363 |
+
|
| 364 |
+
FastMCP supports various date and time types from the `datetime` module:
|
| 365 |
+
|
| 366 |
+
```python
|
| 367 |
+
from datetime import datetime, date, timedelta
|
| 368 |
+
|
| 369 |
+
@mcp.tool()
|
| 370 |
+
def process_date_time(
|
| 371 |
+
event_date: date, # ISO format date string or date object
|
| 372 |
+
event_time: datetime, # ISO format datetime string or datetime object
|
| 373 |
+
duration: timedelta = timedelta(hours=1) # Integer seconds or timedelta
|
| 374 |
+
) -> str:
|
| 375 |
+
"""Process date and time information."""
|
| 376 |
+
# Types are automatically converted from strings
|
| 377 |
+
assert isinstance(event_date, date)
|
| 378 |
+
assert isinstance(event_time, datetime)
|
| 379 |
+
assert isinstance(duration, timedelta)
|
| 380 |
+
|
| 381 |
+
return f"Event on {event_date} at {event_time} for {duration}"
|
| 382 |
+
```
|
| 383 |
+
|
| 384 |
+
- `datetime` - Accepts ISO format strings (e.g., "2023-04-15T14:30:00")
|
| 385 |
+
- `date` - Accepts ISO format date strings (e.g., "2023-04-15")
|
| 386 |
+
- `timedelta` - Accepts integer seconds or timedelta objects
|
| 387 |
+
|
| 388 |
### Collection Types
|
| 389 |
|
| 390 |
FastMCP supports all standard Python collection types:
|
|
|
|
| 530 |
|
| 531 |
This approach is recommended when you expect to receive base64-encoded binary data from clients.
|
| 532 |
|
| 533 |
+
### Paths
|
| 534 |
+
|
| 535 |
+
The `Path` type from the `pathlib` module can be used for file system paths:
|
| 536 |
+
|
| 537 |
+
```python
|
| 538 |
+
from pathlib import Path
|
| 539 |
+
|
| 540 |
+
@mcp.tool()
|
| 541 |
+
def process_file(path: Path) -> str:
|
| 542 |
+
"""Process a file at the given path."""
|
| 543 |
+
assert isinstance(path, Path) # Path is properly converted
|
| 544 |
+
return f"Processing file at {path}"
|
| 545 |
+
```
|
| 546 |
+
|
| 547 |
+
When a client sends a string path, FastMCP automatically converts it to a `Path` object.
|
| 548 |
+
|
| 549 |
+
### UUIDs
|
| 550 |
+
|
| 551 |
+
The `UUID` type from the `uuid` module can be used for unique identifiers:
|
| 552 |
+
|
| 553 |
+
```python
|
| 554 |
+
import uuid
|
| 555 |
+
|
| 556 |
+
@mcp.tool()
|
| 557 |
+
def process_item(
|
| 558 |
+
item_id: uuid.UUID # String UUID or UUID object
|
| 559 |
+
) -> str:
|
| 560 |
+
"""Process an item with the given UUID."""
|
| 561 |
+
assert isinstance(item_id, uuid.UUID) # Properly converted to UUID
|
| 562 |
+
return f"Processing item {item_id}"
|
| 563 |
+
```
|
| 564 |
+
|
| 565 |
+
When a client sends a string UUID (e.g., "123e4567-e89b-12d3-a456-426614174000"), FastMCP automatically converts it to a `UUID` object.
|
| 566 |
+
|
| 567 |
### Pydantic Models
|
| 568 |
|
| 569 |
For complex, structured data with nested fields and validation, use Pydantic models:
|
tests/server/test_server_interactions.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
| 1 |
import base64
|
|
|
|
| 2 |
import json
|
|
|
|
| 3 |
from enum import Enum
|
| 4 |
from pathlib import Path
|
| 5 |
from typing import Annotated, Literal
|
|
@@ -159,6 +161,8 @@ class TestTools:
|
|
| 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 |
|
|
@@ -461,6 +465,145 @@ class TestTools:
|
|
| 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):
|
|
|
|
| 1 |
import base64
|
| 2 |
+
import datetime
|
| 3 |
import json
|
| 4 |
+
import uuid
|
| 5 |
from enum import Enum
|
| 6 |
from pathlib import Path
|
| 7 |
from typing import Annotated, Literal
|
|
|
|
| 161 |
assert isinstance(content3, TextContent)
|
| 162 |
assert content3.text == "direct content"
|
| 163 |
|
| 164 |
+
|
| 165 |
+
class TestToolParameters:
|
| 166 |
async def test_parameter_descriptions_with_field_annotations(self):
|
| 167 |
mcp = FastMCP("Test Server")
|
| 168 |
|
|
|
|
| 465 |
with pytest.raises(ClientError, match="2 validation errors for analyze"):
|
| 466 |
await client.call_tool("analyze", {"x": "not a number"})
|
| 467 |
|
| 468 |
+
async def test_path_type(self):
|
| 469 |
+
mcp = FastMCP()
|
| 470 |
+
|
| 471 |
+
@mcp.tool()
|
| 472 |
+
def send_path(path: Path) -> str:
|
| 473 |
+
assert isinstance(path, Path)
|
| 474 |
+
return str(path)
|
| 475 |
+
|
| 476 |
+
async with Client(mcp) as client:
|
| 477 |
+
result = await client.call_tool("send_path", {"path": "/tmp/test.txt"})
|
| 478 |
+
assert isinstance(result[0], TextContent)
|
| 479 |
+
assert result[0].text == "/tmp/test.txt"
|
| 480 |
+
|
| 481 |
+
async def test_path_type_error(self):
|
| 482 |
+
mcp = FastMCP()
|
| 483 |
+
|
| 484 |
+
@mcp.tool()
|
| 485 |
+
def send_path(path: Path) -> str:
|
| 486 |
+
return str(path)
|
| 487 |
+
|
| 488 |
+
async with Client(mcp) as client:
|
| 489 |
+
with pytest.raises(ClientError, match="Input is not a valid path"):
|
| 490 |
+
await client.call_tool("send_path", {"path": 1})
|
| 491 |
+
|
| 492 |
+
async def test_uuid_type(self):
|
| 493 |
+
mcp = FastMCP()
|
| 494 |
+
|
| 495 |
+
@mcp.tool()
|
| 496 |
+
def send_uuid(x: uuid.UUID) -> str:
|
| 497 |
+
assert isinstance(x, uuid.UUID)
|
| 498 |
+
return str(x)
|
| 499 |
+
|
| 500 |
+
test_uuid = uuid.uuid4()
|
| 501 |
+
|
| 502 |
+
async with Client(mcp) as client:
|
| 503 |
+
result = await client.call_tool("send_uuid", {"x": test_uuid})
|
| 504 |
+
assert isinstance(result[0], TextContent)
|
| 505 |
+
assert result[0].text == str(test_uuid)
|
| 506 |
+
|
| 507 |
+
async def test_uuid_type_error(self):
|
| 508 |
+
mcp = FastMCP()
|
| 509 |
+
|
| 510 |
+
@mcp.tool()
|
| 511 |
+
def send_uuid(x: uuid.UUID) -> str:
|
| 512 |
+
return str(x)
|
| 513 |
+
|
| 514 |
+
async with Client(mcp) as client:
|
| 515 |
+
with pytest.raises(ClientError, match="Input should be a valid UUID"):
|
| 516 |
+
await client.call_tool("send_uuid", {"x": "not a uuid"})
|
| 517 |
+
|
| 518 |
+
async def test_datetime_type(self):
|
| 519 |
+
mcp = FastMCP()
|
| 520 |
+
|
| 521 |
+
@mcp.tool()
|
| 522 |
+
def send_datetime(x: datetime.datetime) -> str:
|
| 523 |
+
return x.isoformat()
|
| 524 |
+
|
| 525 |
+
async with Client(mcp) as client:
|
| 526 |
+
result = await client.call_tool(
|
| 527 |
+
"send_datetime", {"x": datetime.datetime.now()}
|
| 528 |
+
)
|
| 529 |
+
assert isinstance(result[0], TextContent)
|
| 530 |
+
assert result[0].text == datetime.datetime.now().isoformat()
|
| 531 |
+
|
| 532 |
+
async def test_datetime_type_parse_string(self):
|
| 533 |
+
mcp = FastMCP()
|
| 534 |
+
|
| 535 |
+
@mcp.tool()
|
| 536 |
+
def send_datetime(x: datetime.datetime) -> str:
|
| 537 |
+
return x.isoformat()
|
| 538 |
+
|
| 539 |
+
async with Client(mcp) as client:
|
| 540 |
+
result = await client.call_tool(
|
| 541 |
+
"send_datetime", {"x": "2021-01-01T00:00:00"}
|
| 542 |
+
)
|
| 543 |
+
assert isinstance(result[0], TextContent)
|
| 544 |
+
assert result[0].text == "2021-01-01T00:00:00"
|
| 545 |
+
|
| 546 |
+
async def test_datetime_type_error(self):
|
| 547 |
+
mcp = FastMCP()
|
| 548 |
+
|
| 549 |
+
@mcp.tool()
|
| 550 |
+
def send_datetime(x: datetime.datetime) -> str:
|
| 551 |
+
return x.isoformat()
|
| 552 |
+
|
| 553 |
+
async with Client(mcp) as client:
|
| 554 |
+
with pytest.raises(ClientError, match="Input should be a valid datetime"):
|
| 555 |
+
await client.call_tool("send_datetime", {"x": "not a datetime"})
|
| 556 |
+
|
| 557 |
+
async def test_date_type(self):
|
| 558 |
+
mcp = FastMCP()
|
| 559 |
+
|
| 560 |
+
@mcp.tool()
|
| 561 |
+
def send_date(x: datetime.date) -> str:
|
| 562 |
+
return x.isoformat()
|
| 563 |
+
|
| 564 |
+
async with Client(mcp) as client:
|
| 565 |
+
result = await client.call_tool("send_date", {"x": datetime.date.today()})
|
| 566 |
+
assert isinstance(result[0], TextContent)
|
| 567 |
+
assert result[0].text == datetime.date.today().isoformat()
|
| 568 |
+
|
| 569 |
+
async def test_date_type_parse_string(self):
|
| 570 |
+
mcp = FastMCP()
|
| 571 |
+
|
| 572 |
+
@mcp.tool()
|
| 573 |
+
def send_date(x: datetime.date) -> str:
|
| 574 |
+
return x.isoformat()
|
| 575 |
+
|
| 576 |
+
async with Client(mcp) as client:
|
| 577 |
+
result = await client.call_tool("send_date", {"x": "2021-01-01"})
|
| 578 |
+
assert isinstance(result[0], TextContent)
|
| 579 |
+
assert result[0].text == "2021-01-01"
|
| 580 |
+
|
| 581 |
+
async def test_timedelta_type(self):
|
| 582 |
+
mcp = FastMCP()
|
| 583 |
+
|
| 584 |
+
@mcp.tool()
|
| 585 |
+
def send_timedelta(x: datetime.timedelta) -> str:
|
| 586 |
+
return str(x)
|
| 587 |
+
|
| 588 |
+
async with Client(mcp) as client:
|
| 589 |
+
result = await client.call_tool(
|
| 590 |
+
"send_timedelta", {"x": datetime.timedelta(days=1)}
|
| 591 |
+
)
|
| 592 |
+
assert isinstance(result[0], TextContent)
|
| 593 |
+
assert result[0].text == "1 day, 0:00:00"
|
| 594 |
+
|
| 595 |
+
async def test_timedelta_type_parse_int(self):
|
| 596 |
+
mcp = FastMCP()
|
| 597 |
+
|
| 598 |
+
@mcp.tool()
|
| 599 |
+
def send_timedelta(x: datetime.timedelta) -> str:
|
| 600 |
+
return str(x)
|
| 601 |
+
|
| 602 |
+
async with Client(mcp) as client:
|
| 603 |
+
result = await client.call_tool("send_timedelta", {"x": 1000})
|
| 604 |
+
assert isinstance(result[0], TextContent)
|
| 605 |
+
assert result[0].text == "0:16:40"
|
| 606 |
+
|
| 607 |
|
| 608 |
class TestResources:
|
| 609 |
async def test_text_resource(self):
|