Goro commited on
Commit
2dc14d6
·
1 Parent(s): 9fab331

Add file utility type and tests

Browse files
src/fastmcp/tools/tool.py CHANGED
@@ -19,6 +19,7 @@ from fastmcp.utilities.logging import get_logger
19
  from fastmcp.utilities.types import (
20
  Audio,
21
  Image,
 
22
  MCPContent,
23
  find_kwarg_by_type,
24
  get_cached_typeadapter,
@@ -277,6 +278,9 @@ def _convert_to_content(
277
  elif isinstance(result, Audio):
278
  return [result.to_audio_content()]
279
 
 
 
 
280
  if isinstance(result, list | tuple) and not _process_as_single_item:
281
  # if the result is a list, then it could either be a list of MCP types,
282
  # or a "regular" list that the tool is returning, or a mix of both.
@@ -288,7 +292,7 @@ def _convert_to_content(
288
  other_content = []
289
 
290
  for item in result:
291
- if isinstance(item, MCPContent | Image | Audio):
292
  mcp_types.append(_convert_to_content(item)[0])
293
  else:
294
  other_content.append(item)
 
19
  from fastmcp.utilities.types import (
20
  Audio,
21
  Image,
22
+ File,
23
  MCPContent,
24
  find_kwarg_by_type,
25
  get_cached_typeadapter,
 
278
  elif isinstance(result, Audio):
279
  return [result.to_audio_content()]
280
 
281
+ elif isinstance(result, File):
282
+ return [result.to_resource_content()]
283
+
284
  if isinstance(result, list | tuple) and not _process_as_single_item:
285
  # if the result is a list, then it could either be a list of MCP types,
286
  # or a "regular" list that the tool is returning, or a mix of both.
 
292
  other_content = []
293
 
294
  for item in result:
295
+ if isinstance(item, MCPContent | Image | Audio | File):
296
  mcp_types.append(_convert_to_content(item)[0])
297
  else:
298
  other_content.append(item)
src/fastmcp/utilities/types.py CHANGED
@@ -7,6 +7,7 @@ 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
 
11
  from mcp.types import (
12
  Annotations,
@@ -14,6 +15,7 @@ from mcp.types import (
14
  EmbeddedResource,
15
  ImageContent,
16
  TextContent,
 
17
  )
18
  from pydantic import BaseModel, ConfigDict, TypeAdapter
19
 
@@ -203,3 +205,68 @@ class Audio:
203
  mimeType=mime_type or self._mime_type,
204
  annotations=annotations or self.annotations,
205
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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,
 
15
  EmbeddedResource,
16
  ImageContent,
17
  TextContent,
18
+ BlobResourceContents
19
  )
20
  from pydantic import BaseModel, ConfigDict, TypeAdapter
21
 
 
205
  mimeType=mime_type or self._mime_type,
206
  annotations=annotations or self.annotations,
207
  )
208
+
209
+
210
+ class File:
211
+ """Helper class for returning audio from tools."""
212
+
213
+ def __init__(
214
+ self,
215
+ path: str | Path | None = None,
216
+ data: bytes | None = None,
217
+ format: str | None = None,
218
+ name: str | None = None,
219
+ annotations: Annotations | None = None,
220
+ ):
221
+ if path is None and data is None:
222
+ raise ValueError("Either path or data must be provided")
223
+ if path is not None and data is not None:
224
+ raise ValueError("Only one of path or data can be provided")
225
+
226
+ self.path = Path(path) if path else None
227
+ self.data = data
228
+ self._format = format
229
+ self._mime_type = self._get_mime_type()
230
+ self._name = name
231
+ self.annotations = annotations
232
+
233
+ def _get_mime_type(self) -> str:
234
+ """Get MIME type from format or guess from file extension."""
235
+ if self._format:
236
+ return f"application/{self._format.lower()}"
237
+
238
+ if self.path:
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(
246
+ self,
247
+ mime_type: str | None = None,
248
+ annotations: Annotations | None = None,
249
+ ) -> EmbeddedResource:
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
+ )
tests/server/test_server_interactions.py CHANGED
@@ -15,6 +15,7 @@ from mcp.types import (
15
  ImageContent,
16
  TextContent,
17
  TextResourceContents,
 
18
  )
19
  from pydantic import AnyUrl, Field
20
 
@@ -25,7 +26,7 @@ from fastmcp.prompts.prompt import Prompt, PromptMessage
25
  from fastmcp.resources import FileResource, ResourceTemplate
26
  from fastmcp.resources.resource import FunctionResource
27
  from fastmcp.tools.tool import Tool
28
- from fastmcp.utilities.types import Audio, Image
29
 
30
 
31
  @pytest.fixture
@@ -53,10 +54,15 @@ def tool_server():
53
  return Audio(path)
54
 
55
  @mcp.tool
56
- def mixed_content_tool() -> list[TextContent | ImageContent]:
 
 
 
 
57
  return [
58
  TextContent(type="text", text="Hello"),
59
- ImageContent(type="image", data="abc", mimeType="image/png"),
 
60
  ]
61
 
62
  @mcp.tool
@@ -77,6 +83,15 @@ def tool_server():
77
  TextContent(type="text", text="direct content"),
78
  ]
79
 
 
 
 
 
 
 
 
 
 
80
  return mcp
81
 
82
 
@@ -88,7 +103,7 @@ class TestTools:
88
 
89
  async def test_list_tools(self, tool_server: FastMCP):
90
  async with Client(tool_server) as client:
91
- assert len(await client.list_tools()) == 8
92
 
93
  async def test_call_tool(self, tool_server: FastMCP):
94
  async with Client(tool_server) as client:
@@ -304,17 +319,52 @@ class TestToolReturnTypes:
304
  decoded = base64.b64decode(content.data)
305
  assert decoded == b"fake wav data"
306
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  async def test_tool_mixed_content(self, tool_server: FastMCP):
308
  async with Client(tool_server) as client:
309
  result = await client.call_tool("mixed_content_tool", {})
310
- assert len(result) == 2
311
  content1 = result[0]
312
  content2 = result[1]
 
313
  assert isinstance(content1, TextContent)
314
  assert content1.text == "Hello"
315
  assert isinstance(content2, ImageContent)
316
- assert content2.mimeType == "image/png"
317
  assert content2.data == "abc"
 
 
 
 
 
 
 
 
318
 
319
  async def test_tool_mixed_list_with_image(
320
  self, tool_server: FastMCP, tmp_path: Path
@@ -372,6 +422,38 @@ class TestToolReturnTypes:
372
  assert isinstance(content3, TextContent)
373
  assert content3.text == "direct content"
374
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
 
376
  class TestToolParameters:
377
  async def test_parameter_descriptions_with_field_annotations(self):
 
15
  ImageContent,
16
  TextContent,
17
  TextResourceContents,
18
+ BlobResourceContents,
19
  )
20
  from pydantic import AnyUrl, Field
21
 
 
26
  from fastmcp.resources import FileResource, ResourceTemplate
27
  from fastmcp.resources.resource import FunctionResource
28
  from fastmcp.tools.tool import Tool
29
+ from fastmcp.utilities.types import Audio, File, Image
30
 
31
 
32
  @pytest.fixture
 
54
  return Audio(path)
55
 
56
  @mcp.tool
57
+ def file_tool(path: str) -> File:
58
+ return File(path)
59
+
60
+ @mcp.tool
61
+ def mixed_content_tool() -> list[TextContent | ImageContent | EmbeddedResource]:
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
 
83
  TextContent(type="text", text="direct content"),
84
  ]
85
 
86
+ @mcp.tool
87
+ def mixed_file_list_fn(file_path: str) -> list:
88
+ return [
89
+ "text message",
90
+ File(file_path),
91
+ {"key": "value"},
92
+ TextContent(type="text", text="direct content"),
93
+ ]
94
+
95
  return mcp
96
 
97
 
 
103
 
104
  async def test_list_tools(self, tool_server: FastMCP):
105
  async with Client(tool_server) as client:
106
+ assert len(await client.list_tools()) == 10
107
 
108
  async def test_call_tool(self, tool_server: FastMCP):
109
  async with Client(tool_server) as client:
 
319
  decoded = base64.b64decode(content.data)
320
  assert decoded == b"fake wav data"
321
 
322
+ async def test_file(self, tmp_path: Path):
323
+ mcp = FastMCP()
324
+
325
+ @mcp.tool
326
+ def file_tool(path: str) -> File:
327
+ return File(path)
328
+
329
+ # Create a test file
330
+ file_path = tmp_path / "test.bin"
331
+ file_path.write_bytes(b"test file data")
332
+
333
+ async with Client(mcp) as client:
334
+ result = await client.call_tool("file_tool", {"path": str(file_path)})
335
+ content = result[0]
336
+ assert isinstance(content, EmbeddedResource)
337
+ assert content.type == "resource"
338
+ resource = content.resource
339
+ assert resource.mimeType == "application/octet-stream"
340
+ # Verify base64 encoding
341
+ assert hasattr(resource, "blob")
342
+ blob_data = getattr(resource, "blob")
343
+ decoded = base64.b64decode(blob_data)
344
+ assert decoded == b"test file data"
345
+ # Verify URI points to the file
346
+ assert str(resource.uri) == file_path.resolve().as_uri()
347
+
348
  async def test_tool_mixed_content(self, tool_server: FastMCP):
349
  async with Client(tool_server) as client:
350
  result = await client.call_tool("mixed_content_tool", {})
351
+ assert len(result) == 3
352
  content1 = result[0]
353
  content2 = result[1]
354
+ content3 = result[2]
355
  assert isinstance(content1, TextContent)
356
  assert content1.text == "Hello"
357
  assert isinstance(content2, ImageContent)
358
+ assert content2.mimeType == "application/octet-stream"
359
  assert content2.data == "abc"
360
+ assert isinstance(content3, EmbeddedResource)
361
+ assert content3.type == "resource"
362
+ resource = content3.resource
363
+ assert resource.mimeType == "application/octet-stream"
364
+ assert hasattr(resource, "blob")
365
+ blob_data = getattr(resource, "blob")
366
+ decoded = base64.b64decode(blob_data)
367
+ assert decoded == b"abc"
368
 
369
  async def test_tool_mixed_list_with_image(
370
  self, tool_server: FastMCP, tmp_path: Path
 
422
  assert isinstance(content3, TextContent)
423
  assert content3.text == "direct content"
424
 
425
+ async def test_tool_mixed_list_with_file(
426
+ self, tool_server: FastMCP, tmp_path: Path
427
+ ):
428
+ """Test that lists containing File objects and other types are handled
429
+ correctly. Note that the non-MCP content will be grouped together."""
430
+ # Create a test file
431
+ file_path = tmp_path / "test.bin"
432
+ file_path.write_bytes(b"test file data")
433
+
434
+ async with Client(tool_server) as client:
435
+ result = await client.call_tool(
436
+ "mixed_file_list_fn", {"file_path": str(file_path)}
437
+ )
438
+ assert len(result) == 3
439
+ # Check text conversion
440
+ content1 = result[0]
441
+ assert isinstance(content1, TextContent)
442
+ assert json.loads(content1.text) == ["text message", {"key": "value"}]
443
+ # Check file conversion
444
+ content2 = result[1]
445
+ assert isinstance(content2, EmbeddedResource)
446
+ assert content2.type == "resource"
447
+ resource = content2.resource
448
+ assert resource.mimeType == "application/octet-stream"
449
+ assert hasattr(resource, "blob")
450
+ blob_data = getattr(resource, "blob")
451
+ assert base64.b64decode(blob_data) == b"test file data"
452
+ # Check direct TextContent
453
+ content3 = result[2]
454
+ assert isinstance(content3, TextContent)
455
+ assert content3.text == "direct content"
456
+
457
 
458
  class TestToolParameters:
459
  async def test_parameter_descriptions_with_field_annotations(self):
tests/tools/test_tool.py CHANGED
@@ -13,7 +13,7 @@ from fastmcp.client import Client
13
  from fastmcp.exceptions import ToolError
14
  from fastmcp.tools.tool import Tool, _convert_to_content
15
  from fastmcp.utilities.tests import temporary_settings
16
- from fastmcp.utilities.types import Audio, Image
17
 
18
 
19
  class TestToolFromFunction:
@@ -113,6 +113,21 @@ 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
  def test_non_callable_fn(self):
118
  with pytest.raises(TypeError, match="not a callable object"):
@@ -468,6 +483,25 @@ class TestConvertResultToContent:
468
  assert isinstance(result[0], AudioContent)
469
  assert result[0].data == "ZmFrZWF1ZGlvZGF0YQ=="
470
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
  def test_basic_type_result(self):
472
  """Test that a basic type is converted to TextContent."""
473
  result = _convert_to_content(123)
@@ -574,6 +608,38 @@ class TestConvertResultToContent:
574
  audio_item = next(item for item in result if isinstance(item, AudioContent))
575
  assert audio_item.data == "ZmFrZWF1ZGlvZGF0YQ=="
576
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
577
  def test_empty_list(self):
578
  """Test that an empty list results in an empty list."""
579
  result = _convert_to_content([])
 
13
  from fastmcp.exceptions import ToolError
14
  from fastmcp.tools.tool import Tool, _convert_to_content
15
  from fastmcp.utilities.tests import temporary_settings
16
+ from fastmcp.utilities.types import Audio, File, Image
17
 
18
 
19
  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")
120
+
121
+ tool = Tool.from_function(file_tool)
122
+
123
+ result = await tool.run({"data": "test.bin"})
124
+ assert tool.parameters["properties"]["data"]["type"] == "string"
125
+ assert len(result) == 1
126
+ assert isinstance(result[0], EmbeddedResource)
127
+ assert result[0].type == "resource"
128
+ assert hasattr(result[0], "resource")
129
+ resource = result[0].resource
130
+ assert resource.mimeType == "application/octet-stream"
131
 
132
  def test_non_callable_fn(self):
133
  with pytest.raises(TypeError, match="not a callable object"):
 
483
  assert isinstance(result[0], AudioContent)
484
  assert result[0].data == "ZmFrZWF1ZGlvZGF0YQ=="
485
 
486
+ def test_file_object_result(self):
487
+ """Test that a File object is converted to EmbeddedResource with BlobResourceContents."""
488
+ file_obj = File(data=b"filedata", format="octet-stream")
489
+
490
+ result = _convert_to_content(file_obj)
491
+
492
+ assert isinstance(result, list)
493
+ assert len(result) == 1
494
+ assert isinstance(result[0], EmbeddedResource)
495
+ assert result[0].type == "resource"
496
+ assert hasattr(result[0], "resource")
497
+ resource = result[0].resource
498
+ assert resource.mimeType == "application/octet-stream"
499
+ # Check for blob attribute and its value
500
+ assert hasattr(resource, "blob")
501
+ assert getattr(resource, "blob") == "ZmlsZWRhdGE=" # base64 encoded "filedata"
502
+ # Convert URI to string for startswith check
503
+ assert str(resource.uri).startswith("file:///resource.octet-stream")
504
+
505
  def test_basic_type_result(self):
506
  """Test that a basic type is converted to TextContent."""
507
  result = _convert_to_content(123)
 
608
  audio_item = next(item for item in result if isinstance(item, AudioContent))
609
  assert audio_item.data == "ZmFrZWF1ZGlvZGF0YQ=="
610
 
611
+ def test_list_of_mixed_types_with_file(self):
612
+ """Test that a list of mixed types including File is converted correctly."""
613
+ content1 = TextContent(type="text", text="hello")
614
+ file_obj = File(data=b"filedata", format="octet-stream")
615
+ basic_data = {"a": 1}
616
+ result = _convert_to_content([content1, file_obj, basic_data])
617
+
618
+ assert isinstance(result, list)
619
+ assert len(result) == 3
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
+
627
+ assert text_content_count == 2
628
+ assert embedded_content_count == 1
629
+
630
+ text_item = next(item for item in result if isinstance(item, TextContent))
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
638
+ assert resource.mimeType == "application/octet-stream"
639
+ # Check for blob attribute and its value
640
+ assert hasattr(resource, "blob")
641
+ assert getattr(resource, "blob") == "ZmlsZWRhdGE="
642
+
643
  def test_empty_list(self):
644
  """Test that an empty list results in an empty list."""
645
  result = _convert_to_content([])
tests/utilities/test_types.py CHANGED
@@ -6,6 +6,7 @@ import pytest
6
 
7
  from fastmcp.utilities.types import (
8
  Audio,
 
9
  Image,
10
  find_kwarg_by_type,
11
  is_class_member_of_type,
@@ -300,6 +301,103 @@ class TestAudio:
300
  assert content.data == base64.b64encode(test_data).decode()
301
 
302
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  class TestFindKwargByType:
304
  def test_exact_type_match(self):
305
  """Test finding parameter with exact type match."""
 
6
 
7
  from fastmcp.utilities.types import (
8
  Audio,
9
+ File,
10
  Image,
11
  find_kwarg_by_type,
12
  is_class_member_of_type,
 
301
  assert content.data == base64.b64encode(test_data).decode()
302
 
303
 
304
+ class TestFile:
305
+ def test_file_initialization_with_path(self):
306
+ """Test file initialization with a path."""
307
+ # Mock test - we're not actually going to read a file
308
+ file = File(path="test.txt")
309
+ assert file.path is not None
310
+ assert file.data is None
311
+ assert file._mime_type == "text/plain"
312
+
313
+ def test_file_initialization_with_data(self):
314
+ """Test initialization with data and format."""
315
+ test_data = b"test data"
316
+ file = File(data=test_data, format="octet-stream")
317
+ assert file.data == test_data
318
+ # The format parameter should set the MIME type
319
+ assert file._mime_type == "application/octet-stream"
320
+ assert file._name is None
321
+ assert file.annotations is None
322
+
323
+ def test_file_initialization_with_format(self):
324
+ """Test file initialization with a specific format."""
325
+ file = File(data=b"test", format="pdf")
326
+ assert file._mime_type == "application/pdf"
327
+
328
+ def test_file_initialization_with_name(self):
329
+ """Test file initialization with a custom name."""
330
+ file = File(data=b"test", name="custom")
331
+ assert file._name == "custom"
332
+
333
+ def test_missing_data_and_path_raises_error(self):
334
+ """Test that error is raised when neither path nor data is provided."""
335
+ with pytest.raises(ValueError, match="Either path or data must be provided"):
336
+ File()
337
+
338
+ def test_both_data_and_path_raises_error(self):
339
+ """Test that error is raised when both path and data are provided."""
340
+ with pytest.raises(
341
+ ValueError, match="Only one of path or data can be provided"
342
+ ):
343
+ File(path="test.txt", data=b"test")
344
+
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"
352
+
353
+ def test_to_resource_content_with_path(self, tmp_path):
354
+ """Test conversion to ResourceContent with path."""
355
+ file_path = tmp_path / "test.txt"
356
+ test_data = b"test file data"
357
+ file_path.write_bytes(test_data)
358
+
359
+ file = File(path=file_path)
360
+ resource = file.to_resource_content()
361
+
362
+ assert resource.type == "resource"
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."""
370
+ test_data = b"test file data"
371
+ file = File(data=test_data, format="pdf")
372
+ resource = file.to_resource_content()
373
+
374
+ assert resource.type == "resource"
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."""
382
+ file = File(data=b"test")
383
+ monkeypatch.setattr(file, "path", None)
384
+ monkeypatch.setattr(file, "data", None)
385
+
386
+ with pytest.raises(ValueError, match="No resource data available"):
387
+ file.to_resource_content()
388
+
389
+ def test_to_resource_content_with_override_mime_type(self, tmp_path):
390
+ """Test conversion to ResourceContent with override MIME type."""
391
+ file_path = tmp_path / "test.txt"
392
+ test_data = b"test file data"
393
+ file_path.write_bytes(test_data)
394
+
395
+ file = File(path=file_path)
396
+ resource = file.to_resource_content(mime_type="application/custom")
397
+
398
+ assert resource.resource.mimeType == "application/custom"
399
+
400
+
401
  class TestFindKwargByType:
402
  def test_exact_type_match(self):
403
  """Test finding parameter with exact type match."""