Goro commited on
Commit
1b1304a
·
1 Parent(s): 335b92a

Run pre-commit and fix typing issues

Browse files
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(url: str = "https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf") -> File:
 
 
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
- uri=self.path.resolve().as_uri()
254
 
255
  elif self.data is not None:
256
  data = base64.b64encode(self.data).decode()
257
- uri=self.path or (self._name and f"file:///{self._name}.{self._mime_type.split('/')[1]}") or f"file:///resource.{self._mime_type.split('/')[1]}"
 
 
 
258
 
259
  else:
260
  raise ValueError("No resource data available")
261
 
 
 
262
  resource = BlobResourceContents(
263
- blob=data,
264
- mimeType=mime_type or self._mime_type,
265
- uri=uri,
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(type="resource", resource=BlobResourceContents(blob="abc", mimeType="application/octet-stream", uri=AnyUrl("abc"))),
 
 
 
 
 
 
 
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 for item in result
 
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("test content") # Need to write content for MIME type detection
 
 
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
- assert resource.resource.blob == base64.b64encode(test_data).decode()
 
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
- assert resource.resource.blob == base64.b64encode(test_data).decode()
 
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."""