Jeremiah Lowin commited on
Commit
79b3dfe
·
unverified ·
2 Parent(s): 86cc07a2593ecd

Merge pull request #116 from jlowin/list-bug

Browse files
src/fastmcp/server/server.py CHANGED
@@ -8,7 +8,6 @@ from contextlib import (
8
  AbstractAsyncContextManager,
9
  asynccontextmanager,
10
  )
11
- from itertools import chain
12
  from typing import TYPE_CHECKING, Any, Generic, Literal
13
 
14
  import anyio
@@ -618,7 +617,8 @@ class FastMCP(Generic[LifespanResultT]):
618
 
619
  def _convert_to_content(
620
  result: Any,
621
- ) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
 
622
  """Convert a result to a sequence of content objects."""
623
  if result is None:
624
  return []
@@ -629,8 +629,27 @@ def _convert_to_content(
629
  if isinstance(result, Image):
630
  return [result.to_image_content()]
631
 
632
- if isinstance(result, list | tuple):
633
- return list(chain.from_iterable(_convert_to_content(item) for item in result)) # type: ignore[reportUnknownVariableType]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
634
 
635
  if not isinstance(result, str):
636
  try:
 
8
  AbstractAsyncContextManager,
9
  asynccontextmanager,
10
  )
 
11
  from typing import TYPE_CHECKING, Any, Generic, Literal
12
 
13
  import anyio
 
617
 
618
  def _convert_to_content(
619
  result: Any,
620
+ _process_as_single_item: bool = False,
621
+ ) -> list[TextContent | ImageContent | EmbeddedResource]:
622
  """Convert a result to a sequence of content objects."""
623
  if result is None:
624
  return []
 
629
  if isinstance(result, Image):
630
  return [result.to_image_content()]
631
 
632
+ if isinstance(result, list | tuple) and not _process_as_single_item:
633
+ # if the result is a list, then it could either be a list of MCP types,
634
+ # or a "regular" list that the tool is returning, or a mix of both.
635
+ #
636
+ # so we extract all the MCP types / images and convert them as individual content elements,
637
+ # and aggregate the rest as a single content element
638
+
639
+ mcp_types = []
640
+ other_content = []
641
+
642
+ for item in result:
643
+ if isinstance(item, (TextContent, ImageContent, EmbeddedResource, Image)):
644
+ mcp_types.append(_convert_to_content(item)[0])
645
+ else:
646
+ other_content.append(item)
647
+ if other_content:
648
+ other_content = _convert_to_content(
649
+ other_content, _process_as_single_item=True
650
+ )
651
+
652
+ return other_content + mcp_types
653
 
654
  if not isinstance(result, str):
655
  try:
tests/server/test_server.py CHANGED
@@ -1,4 +1,5 @@
1
  import base64
 
2
  from pathlib import Path
3
  from typing import TYPE_CHECKING
4
 
@@ -25,13 +26,11 @@ if TYPE_CHECKING:
25
 
26
 
27
  class TestServer:
28
- @pytest.mark.anyio
29
  async def test_create_server(self):
30
  mcp = FastMCP(instructions="Server instructions")
31
  assert mcp.name == "FastMCP"
32
  assert mcp.instructions == "Server instructions"
33
 
34
- @pytest.mark.anyio
35
  async def test_non_ascii_description(self):
36
  """Test that FastMCP handles non-ASCII characters in descriptions correctly"""
37
  mcp = FastMCP()
@@ -59,7 +58,6 @@ class TestServer:
59
  assert isinstance(content, TextContent)
60
  assert "¡Hola, 世界! 👋" == content.text
61
 
62
- @pytest.mark.anyio
63
  async def test_add_tool_decorator(self):
64
  mcp = FastMCP()
65
 
@@ -69,7 +67,6 @@ class TestServer:
69
 
70
  assert len(mcp._tool_manager.list_tools()) == 1
71
 
72
- @pytest.mark.anyio
73
  async def test_add_tool_decorator_incorrect_usage(self):
74
  mcp = FastMCP()
75
 
@@ -79,7 +76,6 @@ class TestServer:
79
  def add(x: int, y: int) -> int:
80
  return x + y
81
 
82
- @pytest.mark.anyio
83
  async def test_add_resource_decorator(self):
84
  mcp = FastMCP()
85
 
@@ -89,7 +85,6 @@ class TestServer:
89
 
90
  assert len(mcp._resource_manager._templates) == 1
91
 
92
- @pytest.mark.anyio
93
  async def test_add_resource_decorator_incorrect_usage(self):
94
  mcp = FastMCP()
95
 
@@ -106,6 +101,10 @@ def tool_fn(x: int, y: int) -> int:
106
  return x + y
107
 
108
 
 
 
 
 
109
  def error_tool_fn() -> None:
110
  raise ValueError("Test error")
111
 
@@ -122,14 +121,12 @@ def mixed_content_tool_fn() -> list[TextContent | ImageContent]:
122
 
123
 
124
  class TestServerTools:
125
- @pytest.mark.anyio
126
  async def test_add_tool(self):
127
  mcp = FastMCP()
128
  mcp.add_tool(tool_fn)
129
  mcp.add_tool(tool_fn)
130
  assert len(mcp._tool_manager.list_tools()) == 1
131
 
132
- @pytest.mark.anyio
133
  async def test_list_tools(self):
134
  mcp = FastMCP()
135
  mcp.add_tool(tool_fn)
@@ -137,7 +134,6 @@ class TestServerTools:
137
  tools = await client.list_tools()
138
  assert len(tools.tools) == 1
139
 
140
- @pytest.mark.anyio
141
  async def test_call_tool(self):
142
  mcp = FastMCP()
143
  mcp.add_tool(tool_fn)
@@ -146,7 +142,6 @@ class TestServerTools:
146
  assert not hasattr(result, "error")
147
  assert len(result.content) > 0
148
 
149
- @pytest.mark.anyio
150
  async def test_tool_exception_handling(self):
151
  mcp = FastMCP()
152
  mcp.add_tool(error_tool_fn)
@@ -158,7 +153,6 @@ class TestServerTools:
158
  assert "Test error" in content.text
159
  assert result.isError is True
160
 
161
- @pytest.mark.anyio
162
  async def test_tool_error_handling(self):
163
  mcp = FastMCP()
164
  mcp.add_tool(error_tool_fn)
@@ -170,7 +164,6 @@ class TestServerTools:
170
  assert "Test error" in content.text
171
  assert result.isError is True
172
 
173
- @pytest.mark.anyio
174
  async def test_tool_error_details(self):
175
  """Test that exception details are properly formatted in the response"""
176
  mcp = FastMCP()
@@ -183,7 +176,6 @@ class TestServerTools:
183
  assert "Test error" in content.text
184
  assert result.isError is True
185
 
186
- @pytest.mark.anyio
187
  async def test_tool_return_value_conversion(self):
188
  mcp = FastMCP()
189
  mcp.add_tool(tool_fn)
@@ -194,7 +186,16 @@ class TestServerTools:
194
  assert isinstance(content, TextContent)
195
  assert content.text == "3"
196
 
197
- @pytest.mark.anyio
 
 
 
 
 
 
 
 
 
198
  async def test_tool_image_helper(self, tmp_path: Path):
199
  # Create a test image
200
  image_path = tmp_path / "test.png"
@@ -213,12 +214,12 @@ class TestServerTools:
213
  decoded = base64.b64decode(content.data)
214
  assert decoded == b"fake png data"
215
 
216
- @pytest.mark.anyio
217
  async def test_tool_mixed_content(self):
218
  mcp = FastMCP()
219
  mcp.add_tool(mixed_content_tool_fn)
220
  async with client_session(mcp._mcp_server) as client:
221
  result = await client.call_tool("mixed_content_tool_fn", {})
 
222
  assert len(result.content) == 2
223
  content1 = result.content[0]
224
  content2 = result.content[1]
@@ -228,10 +229,9 @@ class TestServerTools:
228
  assert content2.mimeType == "image/png"
229
  assert content2.data == "abc"
230
 
231
- @pytest.mark.anyio
232
  async def test_tool_mixed_list_with_image(self, tmp_path: Path):
233
  """Test that lists containing Image objects and other types are handled
234
- correctly"""
235
  # Create a test image
236
  image_path = tmp_path / "test.png"
237
  image_path.write_bytes(b"test image data")
@@ -248,24 +248,20 @@ class TestServerTools:
248
  mcp.add_tool(mixed_list_fn)
249
  async with client_session(mcp._mcp_server) as client:
250
  result = await client.call_tool("mixed_list_fn", {})
251
- assert len(result.content) == 4
252
  # Check text conversion
253
  content1 = result.content[0]
254
  assert isinstance(content1, TextContent)
255
- assert content1.text == "text message"
256
  # Check image conversion
257
  content2 = result.content[1]
258
  assert isinstance(content2, ImageContent)
259
  assert content2.mimeType == "image/png"
260
  assert base64.b64decode(content2.data) == b"test image data"
261
- # Check dict conversion
262
  content3 = result.content[2]
263
  assert isinstance(content3, TextContent)
264
- assert '"key": "value"' in content3.text
265
- # Check direct TextContent
266
- content4 = result.content[3]
267
- assert isinstance(content4, TextContent)
268
- assert content4.text == "direct content"
269
 
270
  async def test_parameter_descriptions(self):
271
  mcp = FastMCP("Test Server")
@@ -291,7 +287,6 @@ class TestServerTools:
291
 
292
 
293
  class TestServerResources:
294
- @pytest.mark.anyio
295
  async def test_text_resource(self):
296
  mcp = FastMCP()
297
 
@@ -308,7 +303,6 @@ class TestServerResources:
308
  assert isinstance(result.contents[0], TextResourceContents)
309
  assert result.contents[0].text == "Hello, world!"
310
 
311
- @pytest.mark.anyio
312
  async def test_binary_resource(self):
313
  mcp = FastMCP()
314
 
@@ -328,7 +322,6 @@ class TestServerResources:
328
  assert isinstance(result.contents[0], BlobResourceContents)
329
  assert result.contents[0].blob == base64.b64encode(b"Binary data").decode()
330
 
331
- @pytest.mark.anyio
332
  async def test_file_resource_text(self, tmp_path: Path):
333
  mcp = FastMCP()
334
 
@@ -346,7 +339,6 @@ class TestServerResources:
346
  assert isinstance(result.contents[0], TextResourceContents)
347
  assert result.contents[0].text == "Hello from file!"
348
 
349
- @pytest.mark.anyio
350
  async def test_file_resource_binary(self, tmp_path: Path):
351
  mcp = FastMCP()
352
 
@@ -372,7 +364,6 @@ class TestServerResources:
372
 
373
 
374
  class TestServerResourceTemplates:
375
- @pytest.mark.anyio
376
  async def test_resource_with_params(self):
377
  """Test that a resource with function parameters raises an error if the URI
378
  parameters don't match"""
@@ -384,7 +375,6 @@ class TestServerResourceTemplates:
384
  def get_data_fn(param: str) -> str:
385
  return f"Data: {param}"
386
 
387
- @pytest.mark.anyio
388
  async def test_resource_with_uri_params(self):
389
  """Test that a resource with URI parameters is automatically a template"""
390
  mcp = FastMCP()
@@ -395,7 +385,6 @@ class TestServerResourceTemplates:
395
  def get_data() -> str:
396
  return "Data"
397
 
398
- @pytest.mark.anyio
399
  async def test_resource_with_untyped_params(self):
400
  """Test that a resource with untyped parameters raises an error"""
401
  mcp = FastMCP()
@@ -404,7 +393,6 @@ class TestServerResourceTemplates:
404
  def get_data(param) -> str:
405
  return "Data"
406
 
407
- @pytest.mark.anyio
408
  async def test_resource_matching_params(self):
409
  """Test that a resource with matching URI and function parameters works"""
410
  mcp = FastMCP()
@@ -418,7 +406,6 @@ class TestServerResourceTemplates:
418
  assert isinstance(result.contents[0], TextResourceContents)
419
  assert result.contents[0].text == "Data for test"
420
 
421
- @pytest.mark.anyio
422
  async def test_resource_mismatched_params(self):
423
  """Test that mismatched parameters raise an error"""
424
  mcp = FastMCP()
@@ -429,7 +416,6 @@ class TestServerResourceTemplates:
429
  def get_data(user: str) -> str:
430
  return f"Data for {user}"
431
 
432
- @pytest.mark.anyio
433
  async def test_resource_multiple_params(self):
434
  """Test that multiple parameters work correctly"""
435
  mcp = FastMCP()
@@ -445,7 +431,6 @@ class TestServerResourceTemplates:
445
  assert isinstance(result.contents[0], TextResourceContents)
446
  assert result.contents[0].text == "Data for cursor/fastmcp"
447
 
448
- @pytest.mark.anyio
449
  async def test_resource_multiple_mismatched_params(self):
450
  """Test that mismatched parameters raise an error"""
451
  mcp = FastMCP()
@@ -468,7 +453,6 @@ class TestServerResourceTemplates:
468
  assert isinstance(result.contents[0], TextResourceContents)
469
  assert result.contents[0].text == "Static data"
470
 
471
- @pytest.mark.anyio
472
  async def test_template_to_resource_conversion(self):
473
  """Test that templates are properly converted to resources when accessed"""
474
  mcp = FastMCP()
@@ -491,7 +475,6 @@ class TestServerResourceTemplates:
491
  class TestContextInjection:
492
  """Test context injection in tools."""
493
 
494
- @pytest.mark.anyio
495
  async def test_context_detection(self):
496
  """Test that context parameters are properly detected."""
497
  mcp = FastMCP()
@@ -502,7 +485,6 @@ class TestContextInjection:
502
  tool = mcp._tool_manager.add_tool(tool_with_context)
503
  assert tool.context_kwarg == "ctx"
504
 
505
- @pytest.mark.anyio
506
  async def test_context_injection(self):
507
  """Test that context is properly injected into tool calls."""
508
  mcp = FastMCP()
@@ -520,7 +502,6 @@ class TestContextInjection:
520
  assert "Request" in content.text
521
  assert "42" in content.text
522
 
523
- @pytest.mark.anyio
524
  async def test_async_context(self):
525
  """Test that context works in async functions."""
526
  mcp = FastMCP()
@@ -538,7 +519,6 @@ class TestContextInjection:
538
  assert "Async request" in content.text
539
  assert "42" in content.text
540
 
541
- @pytest.mark.anyio
542
  async def test_context_logging(self):
543
  from unittest.mock import patch
544
 
@@ -576,7 +556,6 @@ class TestContextInjection:
576
  level="error", data="Error message", logger=None
577
  )
578
 
579
- @pytest.mark.anyio
580
  async def test_optional_context(self):
581
  """Test that context is optional."""
582
  mcp = FastMCP()
@@ -592,7 +571,6 @@ class TestContextInjection:
592
  assert isinstance(content, TextContent)
593
  assert content.text == "42"
594
 
595
- @pytest.mark.anyio
596
  async def test_context_resource_access(self):
597
  """Test that context can access resources."""
598
  mcp = FastMCP()
@@ -620,7 +598,6 @@ class TestContextInjection:
620
  class TestServerPrompts:
621
  """Test prompt functionality in FastMCP server."""
622
 
623
- @pytest.mark.anyio
624
  async def test_prompt_decorator(self):
625
  """Test that the prompt decorator registers prompts correctly."""
626
  mcp = FastMCP()
@@ -637,7 +614,6 @@ class TestServerPrompts:
637
  assert isinstance(content[0].content, TextContent)
638
  assert content[0].content.text == "Hello, world!"
639
 
640
- @pytest.mark.anyio
641
  async def test_prompt_decorator_with_name(self):
642
  """Test prompt decorator with custom name."""
643
  mcp = FastMCP()
@@ -653,7 +629,6 @@ class TestServerPrompts:
653
  assert isinstance(content[0].content, TextContent)
654
  assert content[0].content.text == "Hello, world!"
655
 
656
- @pytest.mark.anyio
657
  async def test_prompt_decorator_with_description(self):
658
  """Test prompt decorator with custom description."""
659
  mcp = FastMCP()
@@ -678,7 +653,6 @@ class TestServerPrompts:
678
  def fn() -> str:
679
  return "Hello, world!"
680
 
681
- @pytest.mark.anyio
682
  async def test_list_prompts(self):
683
  """Test listing prompts through MCP protocol."""
684
  mcp = FastMCP()
@@ -700,7 +674,6 @@ class TestServerPrompts:
700
  assert prompt.arguments[1].name == "optional"
701
  assert prompt.arguments[1].required is False
702
 
703
- @pytest.mark.anyio
704
  async def test_get_prompt(self):
705
  """Test getting a prompt through MCP protocol."""
706
  mcp = FastMCP()
@@ -718,7 +691,6 @@ class TestServerPrompts:
718
  assert isinstance(content, TextContent)
719
  assert content.text == "Hello, World!"
720
 
721
- @pytest.mark.anyio
722
  async def test_get_prompt_with_resource(self):
723
  """Test getting a prompt that returns resource content."""
724
  mcp = FastMCP()
@@ -748,7 +720,6 @@ class TestServerPrompts:
748
  assert resource.text == "File contents"
749
  assert resource.mimeType == "text/plain"
750
 
751
- @pytest.mark.anyio
752
  async def test_get_unknown_prompt(self):
753
  """Test error when getting unknown prompt."""
754
  mcp = FastMCP()
@@ -756,7 +727,6 @@ class TestServerPrompts:
756
  with pytest.raises(McpError, match="Unknown prompt"):
757
  await client.get_prompt("unknown")
758
 
759
- @pytest.mark.anyio
760
  async def test_get_prompt_missing_args(self):
761
  """Test error when required arguments are missing."""
762
  mcp = FastMCP()
 
1
  import base64
2
+ import json
3
  from pathlib import Path
4
  from typing import TYPE_CHECKING
5
 
 
26
 
27
 
28
  class TestServer:
 
29
  async def test_create_server(self):
30
  mcp = FastMCP(instructions="Server instructions")
31
  assert mcp.name == "FastMCP"
32
  assert mcp.instructions == "Server instructions"
33
 
 
34
  async def test_non_ascii_description(self):
35
  """Test that FastMCP handles non-ASCII characters in descriptions correctly"""
36
  mcp = FastMCP()
 
58
  assert isinstance(content, TextContent)
59
  assert "¡Hola, 世界! 👋" == content.text
60
 
 
61
  async def test_add_tool_decorator(self):
62
  mcp = FastMCP()
63
 
 
67
 
68
  assert len(mcp._tool_manager.list_tools()) == 1
69
 
 
70
  async def test_add_tool_decorator_incorrect_usage(self):
71
  mcp = FastMCP()
72
 
 
76
  def add(x: int, y: int) -> int:
77
  return x + y
78
 
 
79
  async def test_add_resource_decorator(self):
80
  mcp = FastMCP()
81
 
 
85
 
86
  assert len(mcp._resource_manager._templates) == 1
87
 
 
88
  async def test_add_resource_decorator_incorrect_usage(self):
89
  mcp = FastMCP()
90
 
 
101
  return x + y
102
 
103
 
104
+ def tool_fn_list() -> list[str | int]:
105
+ return ["x", 2]
106
+
107
+
108
  def error_tool_fn() -> None:
109
  raise ValueError("Test error")
110
 
 
121
 
122
 
123
  class TestServerTools:
 
124
  async def test_add_tool(self):
125
  mcp = FastMCP()
126
  mcp.add_tool(tool_fn)
127
  mcp.add_tool(tool_fn)
128
  assert len(mcp._tool_manager.list_tools()) == 1
129
 
 
130
  async def test_list_tools(self):
131
  mcp = FastMCP()
132
  mcp.add_tool(tool_fn)
 
134
  tools = await client.list_tools()
135
  assert len(tools.tools) == 1
136
 
 
137
  async def test_call_tool(self):
138
  mcp = FastMCP()
139
  mcp.add_tool(tool_fn)
 
142
  assert not hasattr(result, "error")
143
  assert len(result.content) > 0
144
 
 
145
  async def test_tool_exception_handling(self):
146
  mcp = FastMCP()
147
  mcp.add_tool(error_tool_fn)
 
153
  assert "Test error" in content.text
154
  assert result.isError is True
155
 
 
156
  async def test_tool_error_handling(self):
157
  mcp = FastMCP()
158
  mcp.add_tool(error_tool_fn)
 
164
  assert "Test error" in content.text
165
  assert result.isError is True
166
 
 
167
  async def test_tool_error_details(self):
168
  """Test that exception details are properly formatted in the response"""
169
  mcp = FastMCP()
 
176
  assert "Test error" in content.text
177
  assert result.isError is True
178
 
 
179
  async def test_tool_return_value_conversion(self):
180
  mcp = FastMCP()
181
  mcp.add_tool(tool_fn)
 
186
  assert isinstance(content, TextContent)
187
  assert content.text == "3"
188
 
189
+ async def test_tool_returns_list(self):
190
+ mcp = FastMCP()
191
+ mcp.add_tool(tool_fn_list)
192
+ async with client_session(mcp._mcp_server) as client:
193
+ result = await client.call_tool("tool_fn_list", {})
194
+ assert len(result.content) == 1
195
+ content = result.content[0]
196
+ assert isinstance(content, TextContent)
197
+ assert json.loads(content.text) == ["x", 2]
198
+
199
  async def test_tool_image_helper(self, tmp_path: Path):
200
  # Create a test image
201
  image_path = tmp_path / "test.png"
 
214
  decoded = base64.b64decode(content.data)
215
  assert decoded == b"fake png data"
216
 
 
217
  async def test_tool_mixed_content(self):
218
  mcp = FastMCP()
219
  mcp.add_tool(mixed_content_tool_fn)
220
  async with client_session(mcp._mcp_server) as client:
221
  result = await client.call_tool("mixed_content_tool_fn", {})
222
+
223
  assert len(result.content) == 2
224
  content1 = result.content[0]
225
  content2 = result.content[1]
 
229
  assert content2.mimeType == "image/png"
230
  assert content2.data == "abc"
231
 
 
232
  async def test_tool_mixed_list_with_image(self, tmp_path: Path):
233
  """Test that lists containing Image objects and other types are handled
234
+ correctly. Note that the non-MCP content will be grouped together."""
235
  # Create a test image
236
  image_path = tmp_path / "test.png"
237
  image_path.write_bytes(b"test image data")
 
248
  mcp.add_tool(mixed_list_fn)
249
  async with client_session(mcp._mcp_server) as client:
250
  result = await client.call_tool("mixed_list_fn", {})
251
+ assert len(result.content) == 3
252
  # Check text conversion
253
  content1 = result.content[0]
254
  assert isinstance(content1, TextContent)
255
+ assert json.loads(content1.text) == ["text message", {"key": "value"}]
256
  # Check image conversion
257
  content2 = result.content[1]
258
  assert isinstance(content2, ImageContent)
259
  assert content2.mimeType == "image/png"
260
  assert base64.b64decode(content2.data) == b"test image data"
261
+ # Check direct TextContent
262
  content3 = result.content[2]
263
  assert isinstance(content3, TextContent)
264
+ assert content3.text == "direct content"
 
 
 
 
265
 
266
  async def test_parameter_descriptions(self):
267
  mcp = FastMCP("Test Server")
 
287
 
288
 
289
  class TestServerResources:
 
290
  async def test_text_resource(self):
291
  mcp = FastMCP()
292
 
 
303
  assert isinstance(result.contents[0], TextResourceContents)
304
  assert result.contents[0].text == "Hello, world!"
305
 
 
306
  async def test_binary_resource(self):
307
  mcp = FastMCP()
308
 
 
322
  assert isinstance(result.contents[0], BlobResourceContents)
323
  assert result.contents[0].blob == base64.b64encode(b"Binary data").decode()
324
 
 
325
  async def test_file_resource_text(self, tmp_path: Path):
326
  mcp = FastMCP()
327
 
 
339
  assert isinstance(result.contents[0], TextResourceContents)
340
  assert result.contents[0].text == "Hello from file!"
341
 
 
342
  async def test_file_resource_binary(self, tmp_path: Path):
343
  mcp = FastMCP()
344
 
 
364
 
365
 
366
  class TestServerResourceTemplates:
 
367
  async def test_resource_with_params(self):
368
  """Test that a resource with function parameters raises an error if the URI
369
  parameters don't match"""
 
375
  def get_data_fn(param: str) -> str:
376
  return f"Data: {param}"
377
 
 
378
  async def test_resource_with_uri_params(self):
379
  """Test that a resource with URI parameters is automatically a template"""
380
  mcp = FastMCP()
 
385
  def get_data() -> str:
386
  return "Data"
387
 
 
388
  async def test_resource_with_untyped_params(self):
389
  """Test that a resource with untyped parameters raises an error"""
390
  mcp = FastMCP()
 
393
  def get_data(param) -> str:
394
  return "Data"
395
 
 
396
  async def test_resource_matching_params(self):
397
  """Test that a resource with matching URI and function parameters works"""
398
  mcp = FastMCP()
 
406
  assert isinstance(result.contents[0], TextResourceContents)
407
  assert result.contents[0].text == "Data for test"
408
 
 
409
  async def test_resource_mismatched_params(self):
410
  """Test that mismatched parameters raise an error"""
411
  mcp = FastMCP()
 
416
  def get_data(user: str) -> str:
417
  return f"Data for {user}"
418
 
 
419
  async def test_resource_multiple_params(self):
420
  """Test that multiple parameters work correctly"""
421
  mcp = FastMCP()
 
431
  assert isinstance(result.contents[0], TextResourceContents)
432
  assert result.contents[0].text == "Data for cursor/fastmcp"
433
 
 
434
  async def test_resource_multiple_mismatched_params(self):
435
  """Test that mismatched parameters raise an error"""
436
  mcp = FastMCP()
 
453
  assert isinstance(result.contents[0], TextResourceContents)
454
  assert result.contents[0].text == "Static data"
455
 
 
456
  async def test_template_to_resource_conversion(self):
457
  """Test that templates are properly converted to resources when accessed"""
458
  mcp = FastMCP()
 
475
  class TestContextInjection:
476
  """Test context injection in tools."""
477
 
 
478
  async def test_context_detection(self):
479
  """Test that context parameters are properly detected."""
480
  mcp = FastMCP()
 
485
  tool = mcp._tool_manager.add_tool(tool_with_context)
486
  assert tool.context_kwarg == "ctx"
487
 
 
488
  async def test_context_injection(self):
489
  """Test that context is properly injected into tool calls."""
490
  mcp = FastMCP()
 
502
  assert "Request" in content.text
503
  assert "42" in content.text
504
 
 
505
  async def test_async_context(self):
506
  """Test that context works in async functions."""
507
  mcp = FastMCP()
 
519
  assert "Async request" in content.text
520
  assert "42" in content.text
521
 
 
522
  async def test_context_logging(self):
523
  from unittest.mock import patch
524
 
 
556
  level="error", data="Error message", logger=None
557
  )
558
 
 
559
  async def test_optional_context(self):
560
  """Test that context is optional."""
561
  mcp = FastMCP()
 
571
  assert isinstance(content, TextContent)
572
  assert content.text == "42"
573
 
 
574
  async def test_context_resource_access(self):
575
  """Test that context can access resources."""
576
  mcp = FastMCP()
 
598
  class TestServerPrompts:
599
  """Test prompt functionality in FastMCP server."""
600
 
 
601
  async def test_prompt_decorator(self):
602
  """Test that the prompt decorator registers prompts correctly."""
603
  mcp = FastMCP()
 
614
  assert isinstance(content[0].content, TextContent)
615
  assert content[0].content.text == "Hello, world!"
616
 
 
617
  async def test_prompt_decorator_with_name(self):
618
  """Test prompt decorator with custom name."""
619
  mcp = FastMCP()
 
629
  assert isinstance(content[0].content, TextContent)
630
  assert content[0].content.text == "Hello, world!"
631
 
 
632
  async def test_prompt_decorator_with_description(self):
633
  """Test prompt decorator with custom description."""
634
  mcp = FastMCP()
 
653
  def fn() -> str:
654
  return "Hello, world!"
655
 
 
656
  async def test_list_prompts(self):
657
  """Test listing prompts through MCP protocol."""
658
  mcp = FastMCP()
 
674
  assert prompt.arguments[1].name == "optional"
675
  assert prompt.arguments[1].required is False
676
 
 
677
  async def test_get_prompt(self):
678
  """Test getting a prompt through MCP protocol."""
679
  mcp = FastMCP()
 
691
  assert isinstance(content, TextContent)
692
  assert content.text == "Hello, World!"
693
 
 
694
  async def test_get_prompt_with_resource(self):
695
  """Test getting a prompt that returns resource content."""
696
  mcp = FastMCP()
 
720
  assert resource.text == "File contents"
721
  assert resource.mimeType == "text/plain"
722
 
 
723
  async def test_get_unknown_prompt(self):
724
  """Test error when getting unknown prompt."""
725
  mcp = FastMCP()
 
727
  with pytest.raises(McpError, match="Unknown prompt"):
728
  await client.get_prompt("unknown")
729
 
 
730
  async def test_get_prompt_missing_args(self):
731
  """Test error when required arguments are missing."""
732
  mcp = FastMCP()