Spaces:
Running
Running
Goro commited on
Commit ·
1b1304a
1
Parent(s): 335b92a
Run pre-commit and fix typing issues
Browse files- examples/get_file.py +10 -4
- src/fastmcp/tools/tool.py +1 -1
- src/fastmcp/utilities/types.py +16 -11
- tests/server/test_server_interactions.py +9 -2
- tests/tools/test_tool.py +4 -3
- tests/utilities/test_types.py +8 -3
examples/get_file.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
| 1 |
import aiohttp
|
|
|
|
| 2 |
from fastmcp.server import FastMCP
|
| 3 |
from fastmcp.utilities.types import File
|
| 4 |
|
|
|
|
| 5 |
def create_server():
|
| 6 |
mcp = FastMCP(name="File Demo", instructions="Get files from the server or URL.")
|
| 7 |
|
|
@@ -11,9 +13,11 @@ def create_server():
|
|
| 11 |
Get a test file from the server. If the path is not provided, it defaults to 'requirements.txt'.
|
| 12 |
"""
|
| 13 |
return File(path=path)
|
| 14 |
-
|
| 15 |
@mcp.tool()
|
| 16 |
-
async def get_test_pdf_from_url(
|
|
|
|
|
|
|
| 17 |
"""
|
| 18 |
Get a test PDF file from a URL. If the URL is not provided, it defaults to a sample PDF.
|
| 19 |
"""
|
|
@@ -21,7 +25,9 @@ def create_server():
|
|
| 21 |
async with session.get(url) as response:
|
| 22 |
pdf_data = await response.read()
|
| 23 |
return File(data=pdf_data, format="pdf")
|
| 24 |
-
|
| 25 |
return mcp
|
|
|
|
|
|
|
| 26 |
if __name__ == "__main__":
|
| 27 |
-
create_server().run(transport="sse", host="0.0.0.0", port=8001, path="/sse")
|
|
|
|
| 1 |
import aiohttp
|
| 2 |
+
|
| 3 |
from fastmcp.server import FastMCP
|
| 4 |
from fastmcp.utilities.types import File
|
| 5 |
|
| 6 |
+
|
| 7 |
def create_server():
|
| 8 |
mcp = FastMCP(name="File Demo", instructions="Get files from the server or URL.")
|
| 9 |
|
|
|
|
| 13 |
Get a test file from the server. If the path is not provided, it defaults to 'requirements.txt'.
|
| 14 |
"""
|
| 15 |
return File(path=path)
|
| 16 |
+
|
| 17 |
@mcp.tool()
|
| 18 |
+
async def get_test_pdf_from_url(
|
| 19 |
+
url: str = "https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf",
|
| 20 |
+
) -> File:
|
| 21 |
"""
|
| 22 |
Get a test PDF file from a URL. If the URL is not provided, it defaults to a sample PDF.
|
| 23 |
"""
|
|
|
|
| 25 |
async with session.get(url) as response:
|
| 26 |
pdf_data = await response.read()
|
| 27 |
return File(data=pdf_data, format="pdf")
|
| 28 |
+
|
| 29 |
return mcp
|
| 30 |
+
|
| 31 |
+
|
| 32 |
if __name__ == "__main__":
|
| 33 |
+
create_server().run(transport="sse", host="0.0.0.0", port=8001, path="/sse")
|
src/fastmcp/tools/tool.py
CHANGED
|
@@ -18,8 +18,8 @@ from fastmcp.utilities.json_schema import compress_schema
|
|
| 18 |
from fastmcp.utilities.logging import get_logger
|
| 19 |
from fastmcp.utilities.types import (
|
| 20 |
Audio,
|
| 21 |
-
Image,
|
| 22 |
File,
|
|
|
|
| 23 |
MCPContent,
|
| 24 |
find_kwarg_by_type,
|
| 25 |
get_cached_typeadapter,
|
|
|
|
| 18 |
from fastmcp.utilities.logging import get_logger
|
| 19 |
from fastmcp.utilities.types import (
|
| 20 |
Audio,
|
|
|
|
| 21 |
File,
|
| 22 |
+
Image,
|
| 23 |
MCPContent,
|
| 24 |
find_kwarg_by_type,
|
| 25 |
get_cached_typeadapter,
|
src/fastmcp/utilities/types.py
CHANGED
|
@@ -2,22 +2,22 @@
|
|
| 2 |
|
| 3 |
import base64
|
| 4 |
import inspect
|
|
|
|
| 5 |
from collections.abc import Callable
|
| 6 |
from functools import lru_cache
|
| 7 |
from pathlib import Path
|
| 8 |
from types import UnionType
|
| 9 |
from typing import Annotated, TypeAlias, TypeVar, Union, get_args, get_origin
|
| 10 |
-
import mimetypes
|
| 11 |
|
| 12 |
from mcp.types import (
|
| 13 |
Annotations,
|
| 14 |
AudioContent,
|
|
|
|
| 15 |
EmbeddedResource,
|
| 16 |
ImageContent,
|
| 17 |
TextContent,
|
| 18 |
-
BlobResourceContents
|
| 19 |
)
|
| 20 |
-
from pydantic import BaseModel, ConfigDict, TypeAdapter
|
| 21 |
|
| 22 |
T = TypeVar("T")
|
| 23 |
|
|
@@ -239,7 +239,7 @@ class File:
|
|
| 239 |
mime_type, _ = mimetypes.guess_type(self.path)
|
| 240 |
if mime_type:
|
| 241 |
return mime_type
|
| 242 |
-
|
| 243 |
return "application/octet-stream"
|
| 244 |
|
| 245 |
def to_resource_content(
|
|
@@ -250,23 +250,28 @@ class File:
|
|
| 250 |
if self.path:
|
| 251 |
with open(self.path, "rb") as f:
|
| 252 |
data = base64.b64encode(f.read()).decode()
|
| 253 |
-
|
| 254 |
|
| 255 |
elif self.data is not None:
|
| 256 |
data = base64.b64encode(self.data).decode()
|
| 257 |
-
|
|
|
|
|
|
|
|
|
|
| 258 |
|
| 259 |
else:
|
| 260 |
raise ValueError("No resource data available")
|
| 261 |
|
|
|
|
|
|
|
| 262 |
resource = BlobResourceContents(
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
|
| 268 |
return EmbeddedResource(
|
| 269 |
type="resource",
|
| 270 |
resource=resource,
|
| 271 |
annotations=annotations or self.annotations,
|
| 272 |
-
)
|
|
|
|
| 2 |
|
| 3 |
import base64
|
| 4 |
import inspect
|
| 5 |
+
import mimetypes
|
| 6 |
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,
|
| 14 |
AudioContent,
|
| 15 |
+
BlobResourceContents,
|
| 16 |
EmbeddedResource,
|
| 17 |
ImageContent,
|
| 18 |
TextContent,
|
|
|
|
| 19 |
)
|
| 20 |
+
from pydantic import AnyUrl, BaseModel, ConfigDict, TypeAdapter, UrlConstraints
|
| 21 |
|
| 22 |
T = TypeVar("T")
|
| 23 |
|
|
|
|
| 239 |
mime_type, _ = mimetypes.guess_type(self.path)
|
| 240 |
if mime_type:
|
| 241 |
return mime_type
|
| 242 |
+
|
| 243 |
return "application/octet-stream"
|
| 244 |
|
| 245 |
def to_resource_content(
|
|
|
|
| 250 |
if self.path:
|
| 251 |
with open(self.path, "rb") as f:
|
| 252 |
data = base64.b64encode(f.read()).decode()
|
| 253 |
+
uri_str = self.path.resolve().as_uri()
|
| 254 |
|
| 255 |
elif self.data is not None:
|
| 256 |
data = base64.b64encode(self.data).decode()
|
| 257 |
+
if self._name:
|
| 258 |
+
uri_str = f"file:///{self._name}.{self._mime_type.split('/')[1]}"
|
| 259 |
+
else:
|
| 260 |
+
uri_str = f"file:///resource.{self._mime_type.split('/')[1]}"
|
| 261 |
|
| 262 |
else:
|
| 263 |
raise ValueError("No resource data available")
|
| 264 |
|
| 265 |
+
UriType = Annotated[AnyUrl, UrlConstraints(host_required=False)]
|
| 266 |
+
uri = TypeAdapter(UriType).validate_python(uri_str)
|
| 267 |
resource = BlobResourceContents(
|
| 268 |
+
blob=data,
|
| 269 |
+
mimeType=mime_type or self._mime_type,
|
| 270 |
+
uri=uri,
|
| 271 |
+
)
|
| 272 |
|
| 273 |
return EmbeddedResource(
|
| 274 |
type="resource",
|
| 275 |
resource=resource,
|
| 276 |
annotations=annotations or self.annotations,
|
| 277 |
+
)
|
tests/server/test_server_interactions.py
CHANGED
|
@@ -11,11 +11,11 @@ import pytest
|
|
| 11 |
from mcp import McpError
|
| 12 |
from mcp.types import (
|
| 13 |
AudioContent,
|
|
|
|
| 14 |
EmbeddedResource,
|
| 15 |
ImageContent,
|
| 16 |
TextContent,
|
| 17 |
TextResourceContents,
|
| 18 |
-
BlobResourceContents,
|
| 19 |
)
|
| 20 |
from pydantic import AnyUrl, Field
|
| 21 |
|
|
@@ -62,7 +62,14 @@ def tool_server():
|
|
| 62 |
return [
|
| 63 |
TextContent(type="text", text="Hello"),
|
| 64 |
ImageContent(type="image", data="abc", mimeType="application/octet-stream"),
|
| 65 |
-
EmbeddedResource(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
]
|
| 67 |
|
| 68 |
@mcp.tool
|
|
|
|
| 11 |
from mcp import McpError
|
| 12 |
from mcp.types import (
|
| 13 |
AudioContent,
|
| 14 |
+
BlobResourceContents,
|
| 15 |
EmbeddedResource,
|
| 16 |
ImageContent,
|
| 17 |
TextContent,
|
| 18 |
TextResourceContents,
|
|
|
|
| 19 |
)
|
| 20 |
from pydantic import AnyUrl, Field
|
| 21 |
|
|
|
|
| 62 |
return [
|
| 63 |
TextContent(type="text", text="Hello"),
|
| 64 |
ImageContent(type="image", data="abc", mimeType="application/octet-stream"),
|
| 65 |
+
EmbeddedResource(
|
| 66 |
+
type="resource",
|
| 67 |
+
resource=BlobResourceContents(
|
| 68 |
+
blob=base64.b64encode(b"abc").decode(),
|
| 69 |
+
mimeType="application/octet-stream",
|
| 70 |
+
uri=AnyUrl("file:///test.bin"),
|
| 71 |
+
),
|
| 72 |
+
),
|
| 73 |
]
|
| 74 |
|
| 75 |
@mcp.tool
|
tests/tools/test_tool.py
CHANGED
|
@@ -113,7 +113,7 @@ class TestToolFromFunction:
|
|
| 113 |
result = await tool.run({"data": "test.wav"})
|
| 114 |
assert tool.parameters["properties"]["data"]["type"] == "string"
|
| 115 |
assert isinstance(result[0], AudioContent)
|
| 116 |
-
|
| 117 |
async def test_tool_with_file_return(self):
|
| 118 |
def file_tool(data: bytes) -> File:
|
| 119 |
return File(data=data, format="octet-stream")
|
|
@@ -620,7 +620,7 @@ class TestConvertResultToContent:
|
|
| 620 |
|
| 621 |
text_content_count = sum(isinstance(item, TextContent) for item in result)
|
| 622 |
embedded_content_count = sum(
|
| 623 |
-
isinstance(item, EmbeddedResource) and item.type == "resource"
|
| 624 |
for item in result
|
| 625 |
)
|
| 626 |
|
|
@@ -631,7 +631,8 @@ class TestConvertResultToContent:
|
|
| 631 |
assert text_item.text == '{\n "a": 1\n}'
|
| 632 |
|
| 633 |
embedded_item = next(
|
| 634 |
-
item
|
|
|
|
| 635 |
if isinstance(item, EmbeddedResource) and item.type == "resource"
|
| 636 |
)
|
| 637 |
resource = embedded_item.resource
|
|
|
|
| 113 |
result = await tool.run({"data": "test.wav"})
|
| 114 |
assert tool.parameters["properties"]["data"]["type"] == "string"
|
| 115 |
assert isinstance(result[0], AudioContent)
|
| 116 |
+
|
| 117 |
async def test_tool_with_file_return(self):
|
| 118 |
def file_tool(data: bytes) -> File:
|
| 119 |
return File(data=data, format="octet-stream")
|
|
|
|
| 620 |
|
| 621 |
text_content_count = sum(isinstance(item, TextContent) for item in result)
|
| 622 |
embedded_content_count = sum(
|
| 623 |
+
isinstance(item, EmbeddedResource) and item.type == "resource"
|
| 624 |
for item in result
|
| 625 |
)
|
| 626 |
|
|
|
|
| 631 |
assert text_item.text == '{\n "a": 1\n}'
|
| 632 |
|
| 633 |
embedded_item = next(
|
| 634 |
+
item
|
| 635 |
+
for item in result
|
| 636 |
if isinstance(item, EmbeddedResource) and item.type == "resource"
|
| 637 |
)
|
| 638 |
resource = embedded_item.resource
|
tests/utilities/test_types.py
CHANGED
|
@@ -3,6 +3,7 @@ from types import EllipsisType
|
|
| 3 |
from typing import Annotated, Any
|
| 4 |
|
| 5 |
import pytest
|
|
|
|
| 6 |
|
| 7 |
from fastmcp.utilities.types import (
|
| 8 |
Audio,
|
|
@@ -345,7 +346,9 @@ class TestFile:
|
|
| 345 |
def test_get_mime_type_from_path(self, tmp_path):
|
| 346 |
"""Test MIME type detection from file extension."""
|
| 347 |
file_path = tmp_path / "test.txt"
|
| 348 |
-
file_path.write_text(
|
|
|
|
|
|
|
| 349 |
file = File(path=file_path)
|
| 350 |
# The MIME type should be detected from the .txt extension
|
| 351 |
assert file._mime_type == "text/plain"
|
|
@@ -363,7 +366,8 @@ class TestFile:
|
|
| 363 |
assert resource.resource.mimeType == "text/plain"
|
| 364 |
# Convert both to strings for comparison
|
| 365 |
assert str(resource.resource.uri) == file_path.resolve().as_uri()
|
| 366 |
-
|
|
|
|
| 367 |
|
| 368 |
def test_to_resource_content_with_data(self):
|
| 369 |
"""Test conversion to ResourceContent with data."""
|
|
@@ -375,7 +379,8 @@ class TestFile:
|
|
| 375 |
assert resource.resource.mimeType == "application/pdf"
|
| 376 |
# Convert URI to string for comparison
|
| 377 |
assert str(resource.resource.uri) == "file:///resource.pdf"
|
| 378 |
-
|
|
|
|
| 379 |
|
| 380 |
def test_to_resource_content_error(self, monkeypatch):
|
| 381 |
"""Test error case in to_resource_content."""
|
|
|
|
| 3 |
from typing import Annotated, Any
|
| 4 |
|
| 5 |
import pytest
|
| 6 |
+
from mcp.types import BlobResourceContents
|
| 7 |
|
| 8 |
from fastmcp.utilities.types import (
|
| 9 |
Audio,
|
|
|
|
| 346 |
def test_get_mime_type_from_path(self, tmp_path):
|
| 347 |
"""Test MIME type detection from file extension."""
|
| 348 |
file_path = tmp_path / "test.txt"
|
| 349 |
+
file_path.write_text(
|
| 350 |
+
"test content"
|
| 351 |
+
) # Need to write content for MIME type detection
|
| 352 |
file = File(path=file_path)
|
| 353 |
# The MIME type should be detected from the .txt extension
|
| 354 |
assert file._mime_type == "text/plain"
|
|
|
|
| 366 |
assert resource.resource.mimeType == "text/plain"
|
| 367 |
# Convert both to strings for comparison
|
| 368 |
assert str(resource.resource.uri) == file_path.resolve().as_uri()
|
| 369 |
+
if isinstance(resource.resource, BlobResourceContents):
|
| 370 |
+
assert resource.resource.blob == base64.b64encode(test_data).decode()
|
| 371 |
|
| 372 |
def test_to_resource_content_with_data(self):
|
| 373 |
"""Test conversion to ResourceContent with data."""
|
|
|
|
| 379 |
assert resource.resource.mimeType == "application/pdf"
|
| 380 |
# Convert URI to string for comparison
|
| 381 |
assert str(resource.resource.uri) == "file:///resource.pdf"
|
| 382 |
+
if isinstance(resource.resource, BlobResourceContents):
|
| 383 |
+
assert resource.resource.blob == base64.b64encode(test_data).decode()
|
| 384 |
|
| 385 |
def test_to_resource_content_error(self, monkeypatch):
|
| 386 |
"""Test error case in to_resource_content."""
|