Jeremiah Lowin commited on
Commit
137b0c2
·
1 Parent(s): 4a4ca9b

Split server interaction tests into new file

Browse files
tests/server/test_server.py CHANGED
@@ -1,25 +1,11 @@
1
- import base64
2
- import json
3
- from pathlib import Path
4
- from typing import TYPE_CHECKING
5
-
6
  import pytest
7
  from mcp.types import (
8
- BlobResourceContents,
9
- ImageContent,
10
  TextContent,
11
  TextResourceContents,
12
  )
13
- from pydantic import AnyUrl, Field
14
-
15
- from fastmcp import Client, Context, FastMCP
16
- from fastmcp.exceptions import ClientError, NotFoundError, ToolError
17
- from fastmcp.prompts.prompt import EmbeddedResource, Message, UserMessage
18
- from fastmcp.resources import FileResource, FunctionResource
19
- from fastmcp.utilities.types import Image
20
 
21
- if TYPE_CHECKING:
22
- from fastmcp import Context
23
 
24
 
25
  class TestCreateServer:
@@ -715,825 +701,3 @@ class TestPromptDecorator:
715
  assert len(prompts_dict) == 1
716
  prompt = prompts_dict["sample_prompt"]
717
  assert prompt.tags == {"example", "test-tag"}
718
-
719
-
720
- @pytest.fixture
721
- def tool_server():
722
- mcp = FastMCP()
723
-
724
- @mcp.tool()
725
- def add(x: int, y: int) -> int:
726
- return x + y
727
-
728
- @mcp.tool()
729
- def list_tool() -> list[str | int]:
730
- return ["x", 2]
731
-
732
- @mcp.tool()
733
- def error_tool() -> None:
734
- raise ValueError("Test error")
735
-
736
- @mcp.tool()
737
- def image_tool(path: str) -> Image:
738
- return Image(path)
739
-
740
- @mcp.tool()
741
- def mixed_content_tool() -> list[TextContent | ImageContent]:
742
- return [
743
- TextContent(type="text", text="Hello"),
744
- ImageContent(type="image", data="abc", mimeType="image/png"),
745
- ]
746
-
747
- @mcp.tool()
748
- def mixed_list_fn(image_path: str) -> list:
749
- return [
750
- "text message",
751
- Image(image_path),
752
- {"key": "value"},
753
- TextContent(type="text", text="direct content"),
754
- ]
755
-
756
- return mcp
757
-
758
-
759
- class TestServerTools:
760
- async def test_add_tool_exists(self, tool_server: FastMCP):
761
- assert "add" in [t.name for t in await tool_server._mcp_list_tools()]
762
-
763
- async def test_list_tools(self, tool_server: FastMCP):
764
- assert len(await tool_server._mcp_list_tools()) == 6
765
-
766
- async def test_call_tool(self, tool_server: FastMCP):
767
- result = await tool_server._mcp_call_tool("add", {"x": 1, "y": 2})
768
- assert isinstance(result[0], TextContent)
769
- assert result[0].text == "3"
770
-
771
- async def test_call_tool_as_client(self, tool_server: FastMCP):
772
- async with Client(tool_server) as client:
773
- result = await client.call_tool("add", {"x": 1, "y": 2})
774
- assert isinstance(result[0], TextContent)
775
- assert result[0].text == "3"
776
-
777
- async def test_call_tool_error(self, tool_server: FastMCP):
778
- with pytest.raises(ToolError):
779
- await tool_server._mcp_call_tool("error_tool", {})
780
-
781
- async def test_call_tool_error_as_client(self, tool_server: FastMCP):
782
- async with Client(tool_server) as client:
783
- with pytest.raises(Exception):
784
- await client.call_tool("error_tool", {})
785
-
786
- async def test_call_tool_error_as_client_raw(self, tool_server: FastMCP):
787
- async with Client(tool_server) as client:
788
- result = await client.call_tool("error_tool", {}, _return_raw_result=True)
789
- assert result.isError
790
- assert isinstance(result.content[0], TextContent)
791
- assert "Test error" in result.content[0].text
792
-
793
- async def test_tool_returns_list(self, tool_server: FastMCP):
794
- result = await tool_server._mcp_call_tool("list_tool", {})
795
- assert isinstance(result[0], TextContent)
796
- assert result[0].text == '["x", 2]'
797
-
798
- async def test_tool_image_helper(self, tool_server: FastMCP, tmp_path: Path):
799
- # Create a test image
800
- image_path = tmp_path / "test.png"
801
- image_path.write_bytes(b"fake png data")
802
-
803
- result = await tool_server._mcp_call_tool(
804
- "image_tool", {"path": str(image_path)}
805
- )
806
- content = result[0]
807
- assert isinstance(content, ImageContent)
808
- assert content.type == "image"
809
- assert content.mimeType == "image/png"
810
- # Verify base64 encoding
811
- decoded = base64.b64decode(content.data)
812
- assert decoded == b"fake png data"
813
-
814
- async def test_tool_mixed_content(self, tool_server: FastMCP):
815
- result = await tool_server._mcp_call_tool("mixed_content_tool", {})
816
- assert len(result) == 2
817
- content1 = result[0]
818
- content2 = result[1]
819
- assert isinstance(content1, TextContent)
820
- assert content1.text == "Hello"
821
- assert isinstance(content2, ImageContent)
822
- assert content2.mimeType == "image/png"
823
- assert content2.data == "abc"
824
-
825
- async def test_tool_mixed_list_with_image(
826
- self, tool_server: FastMCP, tmp_path: Path
827
- ):
828
- """Test that lists containing Image objects and other types are handled
829
- correctly. Note that the non-MCP content will be grouped together."""
830
- # Create a test image
831
- image_path = tmp_path / "test.png"
832
- image_path.write_bytes(b"test image data")
833
-
834
- result = await tool_server._mcp_call_tool(
835
- "mixed_list_fn", {"image_path": str(image_path)}
836
- )
837
- assert len(result) == 3
838
- # Check text conversion
839
- content1 = result[0]
840
- assert isinstance(content1, TextContent)
841
- assert json.loads(content1.text) == ["text message", {"key": "value"}]
842
- # Check image conversion
843
- content2 = result[1]
844
- assert isinstance(content2, ImageContent)
845
- assert content2.mimeType == "image/png"
846
- assert base64.b64decode(content2.data) == b"test image data"
847
- # Check direct TextContent
848
- content3 = result[2]
849
- assert isinstance(content3, TextContent)
850
- assert content3.text == "direct content"
851
-
852
- async def test_parameter_descriptions(self):
853
- mcp = FastMCP("Test Server")
854
-
855
- @mcp.tool()
856
- def greet(
857
- name: str = Field(description="The name to greet"),
858
- title: str = Field(description="Optional title", default=""),
859
- ) -> str:
860
- """A greeting tool"""
861
- return f"Hello {title} {name}"
862
-
863
- tools = await mcp._mcp_list_tools()
864
- assert len(tools) == 1
865
- tool = tools[0]
866
-
867
- # Check that parameter descriptions are present in the schema
868
- properties = tool.inputSchema["properties"]
869
- assert "name" in properties
870
- assert properties["name"]["description"] == "The name to greet"
871
- assert "title" in properties
872
- assert properties["title"]["description"] == "Optional title"
873
-
874
-
875
- class TestServerResources:
876
- async def test_text_resource(self):
877
- mcp = FastMCP()
878
-
879
- def get_text():
880
- return "Hello, world!"
881
-
882
- resource = FunctionResource(
883
- uri=AnyUrl("resource://test"), name="test", fn=get_text
884
- )
885
- mcp.add_resource(resource)
886
-
887
- async with Client(mcp) as client:
888
- result = await client.read_resource(AnyUrl("resource://test"))
889
- assert isinstance(result[0], TextResourceContents)
890
- assert result[0].text == "Hello, world!"
891
-
892
- async def test_binary_resource(self):
893
- mcp = FastMCP()
894
-
895
- def get_binary():
896
- return b"Binary data"
897
-
898
- resource = FunctionResource(
899
- uri=AnyUrl("resource://binary"),
900
- name="binary",
901
- fn=get_binary,
902
- mime_type="application/octet-stream",
903
- )
904
- mcp.add_resource(resource)
905
-
906
- async with Client(mcp) as client:
907
- result = await client.read_resource(AnyUrl("resource://binary"))
908
- assert isinstance(result[0], BlobResourceContents)
909
- assert result[0].blob == base64.b64encode(b"Binary data").decode()
910
-
911
- async def test_file_resource_text(self, tmp_path: Path):
912
- mcp = FastMCP()
913
-
914
- # Create a text file
915
- text_file = tmp_path / "test.txt"
916
- text_file.write_text("Hello from file!")
917
-
918
- resource = FileResource(
919
- uri=AnyUrl("file://test.txt"), name="test.txt", path=text_file
920
- )
921
- mcp.add_resource(resource)
922
-
923
- async with Client(mcp) as client:
924
- result = await client.read_resource(AnyUrl("file://test.txt"))
925
- assert isinstance(result[0], TextResourceContents)
926
- assert result[0].text == "Hello from file!"
927
-
928
- async def test_file_resource_binary(self, tmp_path: Path):
929
- mcp = FastMCP()
930
-
931
- # Create a binary file
932
- binary_file = tmp_path / "test.bin"
933
- binary_file.write_bytes(b"Binary file data")
934
-
935
- resource = FileResource(
936
- uri=AnyUrl("file://test.bin"),
937
- name="test.bin",
938
- path=binary_file,
939
- mime_type="application/octet-stream",
940
- )
941
- mcp.add_resource(resource)
942
-
943
- async with Client(mcp) as client:
944
- result = await client.read_resource(AnyUrl("file://test.bin"))
945
- assert isinstance(result[0], BlobResourceContents)
946
- assert result[0].blob == base64.b64encode(b"Binary file data").decode()
947
-
948
-
949
- class TestServerResourceTemplates:
950
- async def test_resource_with_params_not_in_uri(self):
951
- """Test that a resource with function parameters raises an error if the URI
952
- parameters don't match"""
953
- mcp = FastMCP()
954
-
955
- with pytest.raises(
956
- ValueError,
957
- match="URI template must contain at least one parameter",
958
- ):
959
-
960
- @mcp.resource("resource://data")
961
- def get_data_fn(param: str) -> str:
962
- return f"Data: {param}"
963
-
964
- async def test_resource_with_uri_params_without_args(self):
965
- """Test that a resource with URI parameters is automatically a template"""
966
- mcp = FastMCP()
967
-
968
- with pytest.raises(
969
- ValueError,
970
- match="URI parameters .* must be a subset of the function arguments",
971
- ):
972
-
973
- @mcp.resource("resource://{param}")
974
- def get_data() -> str:
975
- return "Data"
976
-
977
- async def test_resource_with_untyped_params(self):
978
- """Test that a resource with untyped parameters raises an error"""
979
- mcp = FastMCP()
980
-
981
- @mcp.resource("resource://{param}")
982
- def get_data(param) -> str:
983
- return "Data"
984
-
985
- async def test_resource_matching_params(self):
986
- """Test that a resource with matching URI and function parameters works"""
987
- mcp = FastMCP()
988
-
989
- @mcp.resource("resource://{name}/data")
990
- def get_data(name: str) -> str:
991
- return f"Data for {name}"
992
-
993
- async with Client(mcp) as client:
994
- result = await client.read_resource(AnyUrl("resource://test/data"))
995
- assert isinstance(result[0], TextResourceContents)
996
- assert result[0].text == "Data for test"
997
-
998
- async def test_resource_mismatched_params(self):
999
- """Test that mismatched parameters raise an error"""
1000
- mcp = FastMCP()
1001
-
1002
- with pytest.raises(
1003
- ValueError,
1004
- match="URI parameters .* must be a subset of the required function arguments",
1005
- ):
1006
-
1007
- @mcp.resource("resource://{name}/data")
1008
- def get_data(user: str) -> str:
1009
- return f"Data for {user}"
1010
-
1011
- async def test_resource_multiple_params(self):
1012
- """Test that multiple parameters work correctly"""
1013
- mcp = FastMCP()
1014
-
1015
- @mcp.resource("resource://{org}/{repo}/data")
1016
- def get_data(org: str, repo: str) -> str:
1017
- return f"Data for {org}/{repo}"
1018
-
1019
- async with Client(mcp) as client:
1020
- result = await client.read_resource(
1021
- AnyUrl("resource://cursor/fastmcp/data")
1022
- )
1023
- assert isinstance(result[0], TextResourceContents)
1024
- assert result[0].text == "Data for cursor/fastmcp"
1025
-
1026
- async def test_resource_multiple_mismatched_params(self):
1027
- """Test that mismatched parameters raise an error"""
1028
- mcp = FastMCP()
1029
-
1030
- with pytest.raises(
1031
- ValueError,
1032
- match="URI parameters .* must be a subset of the required function arguments",
1033
- ):
1034
-
1035
- @mcp.resource("resource://{org}/{repo}/data")
1036
- def get_data_mismatched(org: str, repo_2: str) -> str:
1037
- return f"Data for {org}"
1038
-
1039
- """Test that a resource with no parameters works as a regular resource"""
1040
- mcp = FastMCP()
1041
-
1042
- @mcp.resource("resource://static")
1043
- def get_static_data() -> str:
1044
- return "Static data"
1045
-
1046
- async with Client(mcp) as client:
1047
- result = await client.read_resource(AnyUrl("resource://static"))
1048
- assert isinstance(result[0], TextResourceContents)
1049
- assert result[0].text == "Static data"
1050
-
1051
- async def test_template_with_default_params(self):
1052
- """Test that a template can have default parameters."""
1053
- mcp = FastMCP()
1054
-
1055
- @mcp.resource("math://add/{x}")
1056
- def add(x: int, y: int = 10) -> int:
1057
- return x + y
1058
-
1059
- # Verify it's registered as a template
1060
- templates_dict = await mcp.get_resource_templates()
1061
- templates = list(templates_dict.values())
1062
- assert len(templates) == 1
1063
- assert templates[0].uri_template == "math://add/{x}"
1064
-
1065
- # Call the template and verify it uses the default value
1066
- async with Client(mcp) as client:
1067
- result = await client.read_resource(AnyUrl("math://add/5"))
1068
- assert isinstance(result[0], TextResourceContents)
1069
- assert result[0].text == "15" # 5 + default 10
1070
-
1071
- # Can also call with explicit params
1072
- resource = await mcp._resource_manager.get_resource("math://add/7")
1073
- assert isinstance(resource, FunctionResource)
1074
- result = await resource.read()
1075
- assert result == "17" # 7 + default 10
1076
-
1077
- async def test_template_to_resource_conversion(self):
1078
- """Test that a template can be converted to a resource."""
1079
- mcp = FastMCP()
1080
-
1081
- @mcp.resource("resource://{name}/data")
1082
- def get_data(name: str) -> str:
1083
- return f"Data for {name}"
1084
-
1085
- # Verify it's registered as a template
1086
- templates_dict = await mcp.get_resource_templates()
1087
- templates = list(templates_dict.values())
1088
- assert len(templates) == 1
1089
- assert templates[0].uri_template == "resource://{name}/data"
1090
-
1091
- # When accessed, should create a concrete resource
1092
- resource = await mcp._resource_manager.get_resource("resource://test/data")
1093
- assert isinstance(resource, FunctionResource)
1094
- result = await resource.read()
1095
- assert result == "Data for test"
1096
-
1097
- async def test_stacked_resource_template_decorators(self):
1098
- """Test that resource template decorators can be stacked."""
1099
- mcp = FastMCP()
1100
-
1101
- @mcp.resource("users://email/{email}")
1102
- @mcp.resource("users://name/{name}")
1103
- def lookup_user(name: str | None = None, email: str | None = None) -> dict:
1104
- if name:
1105
- return {
1106
- "id": "123",
1107
- "name": name,
1108
- "email": "dummy@example.com",
1109
- "lookup": "name",
1110
- }
1111
- elif email:
1112
- return {
1113
- "id": "123",
1114
- "name": "Test User",
1115
- "email": email,
1116
- "lookup": "email",
1117
- }
1118
- else:
1119
- raise ValueError("Either name or email must be provided")
1120
-
1121
- # Verify both templates are registered
1122
- templates_dict = await mcp.get_resource_templates()
1123
- templates = list(templates_dict.values())
1124
- assert len(templates) == 2
1125
- template_uris = {t.uri_template for t in templates}
1126
- assert "users://email/{email}" in template_uris
1127
- assert "users://name/{name}" in template_uris
1128
-
1129
- # Test lookup by email
1130
- async with Client(mcp) as client:
1131
- email_result = await client.read_resource(
1132
- AnyUrl("users://email/user@example.com")
1133
- )
1134
- assert isinstance(email_result[0], TextResourceContents)
1135
- email_data = json.loads(email_result[0].text)
1136
- assert email_data["lookup"] == "email"
1137
- assert email_data["email"] == "user@example.com"
1138
-
1139
- # Test lookup by name
1140
- name_result = await client.read_resource(AnyUrl("users://name/John"))
1141
- assert isinstance(name_result[0], TextResourceContents)
1142
- name_data = json.loads(name_result[0].text)
1143
- assert name_data["lookup"] == "name"
1144
- assert name_data["name"] == "John"
1145
- assert name_data["email"] == "dummy@example.com"
1146
-
1147
- async def test_template_decorator_with_tags(self):
1148
- mcp = FastMCP()
1149
-
1150
- @mcp.resource("resource://{param}", tags={"template", "test-tag"})
1151
- def template_resource(param: str) -> str:
1152
- return f"Template resource: {param}"
1153
-
1154
- templates_dict = await mcp.get_resource_templates()
1155
- template = templates_dict["resource://{param}"]
1156
- assert template.tags == {"template", "test-tag"}
1157
-
1158
- async def test_template_decorator_wildcard_param(self):
1159
- mcp = FastMCP()
1160
-
1161
- @mcp.resource("resource://{param*}")
1162
- def template_resource(param: str) -> str:
1163
- return f"Template resource: {param}"
1164
-
1165
- async with Client(mcp) as client:
1166
- result = await client.read_resource(AnyUrl("resource://test/data"))
1167
- assert isinstance(result[0], TextResourceContents)
1168
- assert result[0].text == "Template resource: test/data"
1169
-
1170
- async def test_templates_match_in_order_of_definition(self):
1171
- """
1172
- If a wildcard template is defined first, it will take priority over another
1173
- matching template.
1174
-
1175
- """
1176
- mcp = FastMCP()
1177
-
1178
- @mcp.resource("resource://{param*}")
1179
- def template_resource(param: str) -> str:
1180
- return f"Template resource 1: {param}"
1181
-
1182
- @mcp.resource("resource://{x}/{y}")
1183
- def template_resource_with_params(x: str, y: str) -> str:
1184
- return f"Template resource 2: {x}/{y}"
1185
-
1186
- async with Client(mcp) as client:
1187
- result = await client.read_resource(AnyUrl("resource://a/b/c"))
1188
- assert isinstance(result[0], TextResourceContents)
1189
- assert result[0].text == "Template resource 1: a/b/c"
1190
-
1191
- result = await client.read_resource(AnyUrl("resource://a/b"))
1192
- assert isinstance(result[0], TextResourceContents)
1193
- assert result[0].text == "Template resource 1: a/b"
1194
-
1195
- async def test_templates_shadow_each_other_reorder(self):
1196
- """
1197
- If a wildcard template is defined second, it will *not* take priority over
1198
- another matching template.
1199
- """
1200
- mcp = FastMCP()
1201
-
1202
- @mcp.resource("resource://{x}/{y}")
1203
- def template_resource_with_params(x: str, y: str) -> str:
1204
- return f"Template resource 1: {x}/{y}"
1205
-
1206
- @mcp.resource("resource://{param*}")
1207
- def template_resource(param: str) -> str:
1208
- return f"Template resource 2: {param}"
1209
-
1210
- async with Client(mcp) as client:
1211
- result = await client.read_resource(AnyUrl("resource://a/b/c"))
1212
- assert isinstance(result[0], TextResourceContents)
1213
- assert result[0].text == "Template resource 2: a/b/c"
1214
-
1215
- result = await client.read_resource(AnyUrl("resource://a/b"))
1216
- assert isinstance(result[0], TextResourceContents)
1217
- assert result[0].text == "Template resource 1: a/b"
1218
-
1219
-
1220
- class TestContextInjection:
1221
- """Test context injection in tools."""
1222
-
1223
- async def test_context_detection(self):
1224
- """Test that context parameters are properly detected."""
1225
- mcp = FastMCP()
1226
-
1227
- def tool_with_context(x: int, ctx: Context) -> str:
1228
- return f"Request {ctx.request_id}: {x}"
1229
-
1230
- tool = mcp._tool_manager.add_tool_from_fn(tool_with_context)
1231
- assert tool.context_kwarg == "ctx"
1232
-
1233
- async def test_context_injection(self):
1234
- """Test that context is properly injected into tool calls."""
1235
- mcp = FastMCP()
1236
-
1237
- def tool_with_context(x: int, ctx: Context) -> str:
1238
- assert ctx.request_id is not None
1239
- return f"Request {ctx.request_id}: {x}"
1240
-
1241
- mcp.add_tool(tool_with_context)
1242
- async with Client(mcp) as client:
1243
- result = await client.call_tool("tool_with_context", {"x": 42})
1244
- assert len(result) == 1
1245
- content = result[0]
1246
- assert isinstance(content, TextContent)
1247
- assert "Request" in content.text
1248
- assert "42" in content.text
1249
-
1250
- async def test_async_context(self):
1251
- """Test that context works in async functions."""
1252
- mcp = FastMCP()
1253
-
1254
- async def async_tool(x: int, ctx: Context) -> str:
1255
- assert ctx.request_id is not None
1256
- return f"Async request {ctx.request_id}: {x}"
1257
-
1258
- mcp.add_tool(async_tool)
1259
- async with Client(mcp) as client:
1260
- result = await client.call_tool("async_tool", {"x": 42})
1261
- assert len(result) == 1
1262
- content = result[0]
1263
- assert isinstance(content, TextContent)
1264
- assert "Async request" in content.text
1265
- assert "42" in content.text
1266
-
1267
- async def test_context_logging(self):
1268
- from unittest.mock import patch
1269
-
1270
- import mcp.server.session
1271
-
1272
- """Test that context logging methods work."""
1273
- mcp = FastMCP()
1274
-
1275
- async def logging_tool(msg: str, ctx: Context) -> str:
1276
- await ctx.debug("Debug message")
1277
- await ctx.info("Info message")
1278
- await ctx.warning("Warning message")
1279
- await ctx.error("Error message")
1280
- return f"Logged messages for {msg}"
1281
-
1282
- mcp.add_tool(logging_tool)
1283
-
1284
- with patch("mcp.server.session.ServerSession.send_log_message") as mock_log:
1285
- async with Client(mcp) as client:
1286
- result = await client.call_tool("logging_tool", {"msg": "test"})
1287
- assert len(result) == 1
1288
- content = result[0]
1289
- assert isinstance(content, TextContent)
1290
- assert "Logged messages for test" in content.text
1291
-
1292
- assert mock_log.call_count == 4
1293
- mock_log.assert_any_call(
1294
- level="debug", data="Debug message", logger=None
1295
- )
1296
- mock_log.assert_any_call(level="info", data="Info message", logger=None)
1297
- mock_log.assert_any_call(
1298
- level="warning", data="Warning message", logger=None
1299
- )
1300
- mock_log.assert_any_call(
1301
- level="error", data="Error message", logger=None
1302
- )
1303
-
1304
- async def test_optional_context(self):
1305
- """Test that context is optional."""
1306
- mcp = FastMCP()
1307
-
1308
- def no_context(x: int) -> int:
1309
- return x * 2
1310
-
1311
- mcp.add_tool(no_context)
1312
- async with Client(mcp) as client:
1313
- result = await client.call_tool("no_context", {"x": 21})
1314
- assert len(result) == 1
1315
- content = result[0]
1316
- assert isinstance(content, TextContent)
1317
- assert content.text == "42"
1318
-
1319
- async def test_context_resource_access(self):
1320
- """Test that context can access resources."""
1321
- mcp = FastMCP()
1322
-
1323
- @mcp.resource("test://data")
1324
- def test_resource() -> str:
1325
- return "resource data"
1326
-
1327
- @mcp.tool()
1328
- async def tool_with_resource(ctx: Context) -> str:
1329
- r_iter = await ctx.read_resource("test://data")
1330
- r_list = list(r_iter)
1331
- assert len(r_list) == 1
1332
- r = r_list[0]
1333
- return f"Read resource: {r.content} with mime type {r.mime_type}"
1334
-
1335
- async with Client(mcp) as client:
1336
- result = await client.call_tool("tool_with_resource", {})
1337
- assert len(result) == 1
1338
- content = result[0]
1339
- assert isinstance(content, TextContent)
1340
- assert "Read resource: resource data" in content.text
1341
-
1342
-
1343
- class TestServerPrompts:
1344
- """Test prompt functionality in FastMCP server."""
1345
-
1346
- async def test_prompt_decorator(self):
1347
- """Test that the prompt decorator registers prompts correctly."""
1348
- mcp = FastMCP()
1349
-
1350
- @mcp.prompt()
1351
- def fn() -> str:
1352
- return "Hello, world!"
1353
-
1354
- prompts_dict = await mcp.get_prompts()
1355
- assert len(prompts_dict) == 1
1356
- prompt = prompts_dict["fn"]
1357
- assert prompt.name == "fn"
1358
- # Don't compare functions directly since validate_call wraps them
1359
- content = await prompt.render()
1360
- assert isinstance(content[0].content, TextContent)
1361
- assert content[0].content.text == "Hello, world!"
1362
-
1363
- async def test_prompt_decorator_with_name(self):
1364
- """Test prompt decorator with custom name."""
1365
- mcp = FastMCP()
1366
-
1367
- @mcp.prompt(name="custom_name")
1368
- def fn() -> str:
1369
- return "Hello, world!"
1370
-
1371
- prompts_dict = await mcp.get_prompts()
1372
- assert len(prompts_dict) == 1
1373
- prompt = prompts_dict["custom_name"]
1374
- assert prompt.name == "custom_name"
1375
- content = await prompt.render()
1376
- assert isinstance(content[0].content, TextContent)
1377
- assert content[0].content.text == "Hello, world!"
1378
-
1379
- async def test_prompt_decorator_with_description(self):
1380
- """Test prompt decorator with custom description."""
1381
- mcp = FastMCP()
1382
-
1383
- @mcp.prompt(description="A custom description")
1384
- def fn() -> str:
1385
- return "Hello, world!"
1386
-
1387
- prompts_dict = await mcp.get_prompts()
1388
- assert len(prompts_dict) == 1
1389
- prompt = prompts_dict["fn"]
1390
- assert prompt.description == "A custom description"
1391
- content = await prompt.render()
1392
- assert isinstance(content[0].content, TextContent)
1393
- assert content[0].content.text == "Hello, world!"
1394
-
1395
- def test_prompt_decorator_error(self):
1396
- """Test error when decorator is used incorrectly."""
1397
- mcp = FastMCP()
1398
- with pytest.raises(TypeError, match="decorator was used incorrectly"):
1399
-
1400
- @mcp.prompt # type: ignore
1401
- def fn() -> str:
1402
- return "Hello, world!"
1403
-
1404
- async def test_list_prompts(self):
1405
- """Test listing prompts through MCP protocol."""
1406
- mcp = FastMCP()
1407
-
1408
- @mcp.prompt()
1409
- def fn(name: str, optional: str = "default") -> str:
1410
- return f"Hello, {name}! {optional}"
1411
-
1412
- prompts_dict = await mcp.get_prompts()
1413
- assert len(prompts_dict) == 1
1414
-
1415
- async with Client(mcp) as client:
1416
- prompts = await client.list_prompts()
1417
- assert len(prompts) == 1
1418
- assert prompts[0].name == "fn"
1419
- assert prompts[0].description is None
1420
- assert prompts[0].arguments is not None
1421
- assert len(prompts[0].arguments) == 2
1422
- assert prompts[0].arguments[0].name == "name"
1423
- assert prompts[0].arguments[0].required is True
1424
- assert prompts[0].arguments[1].name == "optional"
1425
- assert prompts[0].arguments[1].required is False
1426
-
1427
- async def test_get_prompt(self):
1428
- """Test getting a prompt through MCP protocol."""
1429
- mcp = FastMCP()
1430
-
1431
- @mcp.prompt()
1432
- def fn(name: str) -> str:
1433
- return f"Hello, {name}!"
1434
-
1435
- async with Client(mcp) as client:
1436
- result = await client.get_prompt("fn", {"name": "World"})
1437
- assert len(result) == 1
1438
- message = result[0]
1439
- assert message.role == "user"
1440
- content = message.content
1441
- assert isinstance(content, TextContent)
1442
- assert content.text == "Hello, World!"
1443
-
1444
- async def test_get_prompt_with_resource(self):
1445
- """Test getting a prompt that returns resource content."""
1446
- mcp = FastMCP()
1447
-
1448
- @mcp.prompt()
1449
- def fn() -> Message:
1450
- return UserMessage(
1451
- content=EmbeddedResource(
1452
- type="resource",
1453
- resource=TextResourceContents(
1454
- uri=AnyUrl("file://file.txt"),
1455
- text="File contents",
1456
- mimeType="text/plain",
1457
- ),
1458
- )
1459
- )
1460
-
1461
- async with Client(mcp) as client:
1462
- result = await client.get_prompt("fn")
1463
- assert result[0].role == "user"
1464
- content = result[0].content
1465
- assert isinstance(content, EmbeddedResource)
1466
- resource = content.resource
1467
- assert isinstance(resource, TextResourceContents)
1468
- assert resource.text == "File contents"
1469
- assert resource.mimeType == "text/plain"
1470
-
1471
- async def test_get_unknown_prompt(self):
1472
- """Test error when getting unknown prompt."""
1473
- mcp = FastMCP()
1474
- with pytest.raises(ClientError, match="Unknown prompt"):
1475
- async with Client(mcp) as client:
1476
- await client.get_prompt("unknown")
1477
-
1478
- async def test_get_prompt_missing_args(self):
1479
- """Test error when required arguments are missing."""
1480
- mcp = FastMCP()
1481
-
1482
- @mcp.prompt()
1483
- def prompt_fn(name: str) -> str:
1484
- return f"Hello, {name}!"
1485
-
1486
- with pytest.raises(ClientError, match="Missing required arguments"):
1487
- async with Client(mcp) as client:
1488
- await client.get_prompt("prompt_fn")
1489
-
1490
- async def test_tool_decorator_with_tags(self):
1491
- """Test that the tool decorator properly sets tags."""
1492
- mcp = FastMCP()
1493
-
1494
- @mcp.tool(tags={"example", "test-tag"})
1495
- def sample_tool(x: int) -> int:
1496
- return x * 2
1497
-
1498
- # Verify the tags were set correctly
1499
- tools = mcp._tool_manager.list_tools()
1500
- assert len(tools) == 1
1501
- assert tools[0].tags == {"example", "test-tag"}
1502
-
1503
- async def test_resource_decorator_with_tags(self):
1504
- """Test that the resource decorator supports tags."""
1505
- mcp = FastMCP()
1506
-
1507
- @mcp.resource("resource://data", tags={"example", "test-tag"})
1508
- def get_data() -> str:
1509
- return "Hello, world!"
1510
-
1511
- resources_dict = await mcp.get_resources()
1512
- resources = list(resources_dict.values())
1513
- assert len(resources) == 1
1514
- assert resources[0].tags == {"example", "test-tag"}
1515
-
1516
- async def test_template_decorator_with_tags(self):
1517
- """Test that the template decorator properly sets tags."""
1518
- mcp = FastMCP()
1519
-
1520
- @mcp.resource("resource://{param}", tags={"template", "test-tag"})
1521
- def template_resource(param: str) -> str:
1522
- return f"Template resource: {param}"
1523
-
1524
- templates_dict = await mcp.get_resource_templates()
1525
- template = templates_dict["resource://{param}"]
1526
- assert template.tags == {"template", "test-tag"}
1527
-
1528
- async def test_prompt_decorator_with_tags(self):
1529
- """Test that the prompt decorator properly sets tags."""
1530
- mcp = FastMCP()
1531
-
1532
- @mcp.prompt(tags={"example", "test-tag"})
1533
- def sample_prompt() -> str:
1534
- return "Hello, world!"
1535
-
1536
- prompts_dict = await mcp.get_prompts()
1537
- assert len(prompts_dict) == 1
1538
- prompt = prompts_dict["sample_prompt"]
1539
- assert prompt.tags == {"example", "test-tag"}
 
 
 
 
 
 
1
  import pytest
2
  from mcp.types import (
 
 
3
  TextContent,
4
  TextResourceContents,
5
  )
 
 
 
 
 
 
 
6
 
7
+ from fastmcp import Client, FastMCP
8
+ from fastmcp.exceptions import ClientError, NotFoundError
9
 
10
 
11
  class TestCreateServer:
 
701
  assert len(prompts_dict) == 1
702
  prompt = prompts_dict["sample_prompt"]
703
  assert prompt.tags == {"example", "test-tag"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/server/test_server_interactions.py ADDED
@@ -0,0 +1,851 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import json
3
+ from pathlib import Path
4
+
5
+ import pytest
6
+ from mcp.types import (
7
+ BlobResourceContents,
8
+ ImageContent,
9
+ TextContent,
10
+ TextResourceContents,
11
+ )
12
+ from pydantic import AnyUrl, Field
13
+
14
+ from fastmcp import Client, Context, FastMCP
15
+ from fastmcp.exceptions import ClientError
16
+ from fastmcp.prompts.prompt import EmbeddedResource, Message, UserMessage
17
+ from fastmcp.resources import FileResource, FunctionResource
18
+ from fastmcp.utilities.types import Image
19
+
20
+
21
+ @pytest.fixture
22
+ def tool_server():
23
+ mcp = FastMCP()
24
+
25
+ @mcp.tool()
26
+ def add(x: int, y: int) -> int:
27
+ return x + y
28
+
29
+ @mcp.tool()
30
+ def list_tool() -> list[str | int]:
31
+ return ["x", 2]
32
+
33
+ @mcp.tool()
34
+ def error_tool() -> None:
35
+ raise ValueError("Test error")
36
+
37
+ @mcp.tool()
38
+ def image_tool(path: str) -> Image:
39
+ return Image(path)
40
+
41
+ @mcp.tool()
42
+ def mixed_content_tool() -> list[TextContent | ImageContent]:
43
+ return [
44
+ TextContent(type="text", text="Hello"),
45
+ ImageContent(type="image", data="abc", mimeType="image/png"),
46
+ ]
47
+
48
+ @mcp.tool()
49
+ def mixed_list_fn(image_path: str) -> list:
50
+ return [
51
+ "text message",
52
+ Image(image_path),
53
+ {"key": "value"},
54
+ TextContent(type="text", text="direct content"),
55
+ ]
56
+
57
+ return mcp
58
+
59
+
60
+ class TestTools:
61
+ async def test_add_tool_exists(self, tool_server: FastMCP):
62
+ async with Client(tool_server) as client:
63
+ tools = await client.list_tools()
64
+ assert "add" in [t.name for t in tools]
65
+
66
+ async def test_list_tools(self, tool_server: FastMCP):
67
+ async with Client(tool_server) as client:
68
+ assert len(await client.list_tools()) == 6
69
+
70
+ async def test_call_tool(self, tool_server: FastMCP):
71
+ async with Client(tool_server) as client:
72
+ result = await client.call_tool("add", {"x": 1, "y": 2})
73
+ assert isinstance(result[0], TextContent)
74
+ assert result[0].text == "3"
75
+
76
+ async def test_call_tool_as_client(self, tool_server: FastMCP):
77
+ async with Client(tool_server) as client:
78
+ result = await client.call_tool("add", {"x": 1, "y": 2})
79
+ assert isinstance(result[0], TextContent)
80
+ assert result[0].text == "3"
81
+
82
+ async def test_call_tool_error(self, tool_server: FastMCP):
83
+ async with Client(tool_server) as client:
84
+ with pytest.raises(Exception):
85
+ await client.call_tool("error_tool", {})
86
+
87
+ async def test_call_tool_error_as_client(self, tool_server: FastMCP):
88
+ async with Client(tool_server) as client:
89
+ with pytest.raises(Exception):
90
+ await client.call_tool("error_tool", {})
91
+
92
+ async def test_call_tool_error_as_client_raw(self, tool_server: FastMCP):
93
+ async with Client(tool_server) as client:
94
+ result = await client.call_tool("error_tool", {}, _return_raw_result=True)
95
+ assert result.isError
96
+ assert isinstance(result.content[0], TextContent)
97
+ assert "Test error" in result.content[0].text
98
+
99
+ async def test_tool_returns_list(self, tool_server: FastMCP):
100
+ async with Client(tool_server) as client:
101
+ result = await client.call_tool("list_tool", {})
102
+ assert isinstance(result[0], TextContent)
103
+ assert result[0].text == '["x", 2]'
104
+
105
+ async def test_tool_image_helper(self, tool_server: FastMCP, tmp_path: Path):
106
+ # Create a test image
107
+ image_path = tmp_path / "test.png"
108
+ image_path.write_bytes(b"fake png data")
109
+
110
+ async with Client(tool_server) as client:
111
+ result = await client.call_tool("image_tool", {"path": str(image_path)})
112
+ content = result[0]
113
+ assert isinstance(content, ImageContent)
114
+ assert content.type == "image"
115
+ assert content.mimeType == "image/png"
116
+ # Verify base64 encoding
117
+ decoded = base64.b64decode(content.data)
118
+ assert decoded == b"fake png data"
119
+
120
+ async def test_tool_mixed_content(self, tool_server: FastMCP):
121
+ async with Client(tool_server) as client:
122
+ result = await client.call_tool("mixed_content_tool", {})
123
+ assert len(result) == 2
124
+ content1 = result[0]
125
+ content2 = result[1]
126
+ assert isinstance(content1, TextContent)
127
+ assert content1.text == "Hello"
128
+ assert isinstance(content2, ImageContent)
129
+ assert content2.mimeType == "image/png"
130
+ assert content2.data == "abc"
131
+
132
+ async def test_tool_mixed_list_with_image(
133
+ self, tool_server: FastMCP, tmp_path: Path
134
+ ):
135
+ """Test that lists containing Image objects and other types are handled
136
+ correctly. Note that the non-MCP content will be grouped together."""
137
+ # Create a test image
138
+ image_path = tmp_path / "test.png"
139
+ image_path.write_bytes(b"test image data")
140
+
141
+ async with Client(tool_server) as client:
142
+ result = await client.call_tool(
143
+ "mixed_list_fn", {"image_path": str(image_path)}
144
+ )
145
+ assert len(result) == 3
146
+ # Check text conversion
147
+ content1 = result[0]
148
+ assert isinstance(content1, TextContent)
149
+ assert json.loads(content1.text) == ["text message", {"key": "value"}]
150
+ # Check image conversion
151
+ content2 = result[1]
152
+ assert isinstance(content2, ImageContent)
153
+ assert content2.mimeType == "image/png"
154
+ assert base64.b64decode(content2.data) == b"test image data"
155
+ # Check direct TextContent
156
+ content3 = result[2]
157
+ assert isinstance(content3, TextContent)
158
+ assert content3.text == "direct content"
159
+
160
+ async def test_parameter_descriptions(self):
161
+ mcp = FastMCP("Test Server")
162
+
163
+ @mcp.tool()
164
+ def greet(
165
+ name: str = Field(description="The name to greet"),
166
+ title: str = Field(description="Optional title", default=""),
167
+ ) -> str:
168
+ """A greeting tool"""
169
+ return f"Hello {title} {name}"
170
+
171
+ async with Client(mcp) as client:
172
+ tools = await client.list_tools()
173
+ assert len(tools) == 1
174
+ tool = tools[0]
175
+
176
+ # Check that parameter descriptions are present in the schema
177
+ properties = tool.inputSchema["properties"]
178
+ assert "name" in properties
179
+ assert properties["name"]["description"] == "The name to greet"
180
+ assert "title" in properties
181
+ assert properties["title"]["description"] == "Optional title"
182
+
183
+
184
+ class TestResources:
185
+ async def test_text_resource(self):
186
+ mcp = FastMCP()
187
+
188
+ def get_text():
189
+ return "Hello, world!"
190
+
191
+ resource = FunctionResource(
192
+ uri=AnyUrl("resource://test"), name="test", fn=get_text
193
+ )
194
+ mcp.add_resource(resource)
195
+
196
+ async with Client(mcp) as client:
197
+ result = await client.read_resource(AnyUrl("resource://test"))
198
+ assert isinstance(result[0], TextResourceContents)
199
+ assert result[0].text == "Hello, world!"
200
+
201
+ async def test_binary_resource(self):
202
+ mcp = FastMCP()
203
+
204
+ def get_binary():
205
+ return b"Binary data"
206
+
207
+ resource = FunctionResource(
208
+ uri=AnyUrl("resource://binary"),
209
+ name="binary",
210
+ fn=get_binary,
211
+ mime_type="application/octet-stream",
212
+ )
213
+ mcp.add_resource(resource)
214
+
215
+ async with Client(mcp) as client:
216
+ result = await client.read_resource(AnyUrl("resource://binary"))
217
+ assert isinstance(result[0], BlobResourceContents)
218
+ assert result[0].blob == base64.b64encode(b"Binary data").decode()
219
+
220
+ async def test_file_resource_text(self, tmp_path: Path):
221
+ mcp = FastMCP()
222
+
223
+ # Create a text file
224
+ text_file = tmp_path / "test.txt"
225
+ text_file.write_text("Hello from file!")
226
+
227
+ resource = FileResource(
228
+ uri=AnyUrl("file://test.txt"), name="test.txt", path=text_file
229
+ )
230
+ mcp.add_resource(resource)
231
+
232
+ async with Client(mcp) as client:
233
+ result = await client.read_resource(AnyUrl("file://test.txt"))
234
+ assert isinstance(result[0], TextResourceContents)
235
+ assert result[0].text == "Hello from file!"
236
+
237
+ async def test_file_resource_binary(self, tmp_path: Path):
238
+ mcp = FastMCP()
239
+
240
+ # Create a binary file
241
+ binary_file = tmp_path / "test.bin"
242
+ binary_file.write_bytes(b"Binary file data")
243
+
244
+ resource = FileResource(
245
+ uri=AnyUrl("file://test.bin"),
246
+ name="test.bin",
247
+ path=binary_file,
248
+ mime_type="application/octet-stream",
249
+ )
250
+ mcp.add_resource(resource)
251
+
252
+ async with Client(mcp) as client:
253
+ result = await client.read_resource(AnyUrl("file://test.bin"))
254
+ assert isinstance(result[0], BlobResourceContents)
255
+ assert result[0].blob == base64.b64encode(b"Binary file data").decode()
256
+
257
+
258
+ class TestResourceTemplates:
259
+ async def test_resource_with_params_not_in_uri(self):
260
+ """Test that a resource with function parameters raises an error if the URI
261
+ parameters don't match"""
262
+ mcp = FastMCP()
263
+
264
+ with pytest.raises(
265
+ ValueError,
266
+ match="URI template must contain at least one parameter",
267
+ ):
268
+
269
+ @mcp.resource("resource://data")
270
+ def get_data_fn(param: str) -> str:
271
+ return f"Data: {param}"
272
+
273
+ async def test_resource_with_uri_params_without_args(self):
274
+ """Test that a resource with URI parameters is automatically a template"""
275
+ mcp = FastMCP()
276
+
277
+ with pytest.raises(
278
+ ValueError,
279
+ match="URI parameters .* must be a subset of the function arguments",
280
+ ):
281
+
282
+ @mcp.resource("resource://{param}")
283
+ def get_data() -> str:
284
+ return "Data"
285
+
286
+ async def test_resource_with_untyped_params(self):
287
+ """Test that a resource with untyped parameters raises an error"""
288
+ mcp = FastMCP()
289
+
290
+ @mcp.resource("resource://{param}")
291
+ def get_data(param) -> str:
292
+ return "Data"
293
+
294
+ async def test_resource_matching_params(self):
295
+ """Test that a resource with matching URI and function parameters works"""
296
+ mcp = FastMCP()
297
+
298
+ @mcp.resource("resource://{name}/data")
299
+ def get_data(name: str) -> str:
300
+ return f"Data for {name}"
301
+
302
+ async with Client(mcp) as client:
303
+ result = await client.read_resource(AnyUrl("resource://test/data"))
304
+ assert isinstance(result[0], TextResourceContents)
305
+ assert result[0].text == "Data for test"
306
+
307
+ async def test_resource_mismatched_params(self):
308
+ """Test that mismatched parameters raise an error"""
309
+ mcp = FastMCP()
310
+
311
+ with pytest.raises(
312
+ ValueError,
313
+ match="URI parameters .* must be a subset of the required function arguments",
314
+ ):
315
+
316
+ @mcp.resource("resource://{name}/data")
317
+ def get_data(user: str) -> str:
318
+ return f"Data for {user}"
319
+
320
+ async def test_resource_multiple_params(self):
321
+ """Test that multiple parameters work correctly"""
322
+ mcp = FastMCP()
323
+
324
+ @mcp.resource("resource://{org}/{repo}/data")
325
+ def get_data(org: str, repo: str) -> str:
326
+ return f"Data for {org}/{repo}"
327
+
328
+ async with Client(mcp) as client:
329
+ result = await client.read_resource(
330
+ AnyUrl("resource://cursor/fastmcp/data")
331
+ )
332
+ assert isinstance(result[0], TextResourceContents)
333
+ assert result[0].text == "Data for cursor/fastmcp"
334
+
335
+ async def test_resource_multiple_mismatched_params(self):
336
+ """Test that mismatched parameters raise an error"""
337
+ mcp = FastMCP()
338
+
339
+ with pytest.raises(
340
+ ValueError,
341
+ match="URI parameters .* must be a subset of the required function arguments",
342
+ ):
343
+
344
+ @mcp.resource("resource://{org}/{repo}/data")
345
+ def get_data_mismatched(org: str, repo_2: str) -> str:
346
+ return f"Data for {org}"
347
+
348
+ """Test that a resource with no parameters works as a regular resource"""
349
+ mcp = FastMCP()
350
+
351
+ @mcp.resource("resource://static")
352
+ def get_static_data() -> str:
353
+ return "Static data"
354
+
355
+ async with Client(mcp) as client:
356
+ result = await client.read_resource(AnyUrl("resource://static"))
357
+ assert isinstance(result[0], TextResourceContents)
358
+ assert result[0].text == "Static data"
359
+
360
+ async def test_template_with_default_params(self):
361
+ """Test that a template can have default parameters."""
362
+ mcp = FastMCP()
363
+
364
+ @mcp.resource("math://add/{x}")
365
+ def add(x: int, y: int = 10) -> int:
366
+ return x + y
367
+
368
+ # Verify it's registered as a template
369
+ templates_dict = await mcp.get_resource_templates()
370
+ templates = list(templates_dict.values())
371
+ assert len(templates) == 1
372
+ assert templates[0].uri_template == "math://add/{x}"
373
+
374
+ # Call the template and verify it uses the default value
375
+ async with Client(mcp) as client:
376
+ result = await client.read_resource(AnyUrl("math://add/5"))
377
+ assert isinstance(result[0], TextResourceContents)
378
+ assert result[0].text == "15" # 5 + default 10
379
+
380
+ # Can also call with explicit params
381
+ result2 = await client.read_resource(AnyUrl("math://add/7"))
382
+ assert isinstance(result2[0], TextResourceContents)
383
+ assert result2[0].text == "17" # 7 + default 10
384
+
385
+ async def test_template_to_resource_conversion(self):
386
+ """Test that a template can be converted to a resource."""
387
+ mcp = FastMCP()
388
+
389
+ @mcp.resource("resource://{name}/data")
390
+ def get_data(name: str) -> str:
391
+ return f"Data for {name}"
392
+
393
+ # Verify it's registered as a template
394
+ templates_dict = await mcp.get_resource_templates()
395
+ templates = list(templates_dict.values())
396
+ assert len(templates) == 1
397
+ assert templates[0].uri_template == "resource://{name}/data"
398
+
399
+ # When accessed, should create a concrete resource
400
+ async with Client(mcp) as client:
401
+ result = await client.read_resource(AnyUrl("resource://test/data"))
402
+ assert isinstance(result[0], TextResourceContents)
403
+ assert result[0].text == "Data for test"
404
+
405
+ async def test_stacked_resource_template_decorators(self):
406
+ """Test that resource template decorators can be stacked."""
407
+ mcp = FastMCP()
408
+
409
+ @mcp.resource("users://email/{email}")
410
+ @mcp.resource("users://name/{name}")
411
+ def lookup_user(name: str | None = None, email: str | None = None) -> dict:
412
+ if name:
413
+ return {
414
+ "id": "123",
415
+ "name": name,
416
+ "email": "dummy@example.com",
417
+ "lookup": "name",
418
+ }
419
+ elif email:
420
+ return {
421
+ "id": "123",
422
+ "name": "Test User",
423
+ "email": email,
424
+ "lookup": "email",
425
+ }
426
+ else:
427
+ raise ValueError("Either name or email must be provided")
428
+
429
+ # Verify both templates are registered
430
+ templates_dict = await mcp.get_resource_templates()
431
+ templates = list(templates_dict.values())
432
+ assert len(templates) == 2
433
+ template_uris = {t.uri_template for t in templates}
434
+ assert "users://email/{email}" in template_uris
435
+ assert "users://name/{name}" in template_uris
436
+
437
+ # Test lookup by email
438
+ async with Client(mcp) as client:
439
+ email_result = await client.read_resource(
440
+ AnyUrl("users://email/user@example.com")
441
+ )
442
+ assert isinstance(email_result[0], TextResourceContents)
443
+ email_data = json.loads(email_result[0].text)
444
+ assert email_data["lookup"] == "email"
445
+ assert email_data["email"] == "user@example.com"
446
+
447
+ # Test lookup by name
448
+ name_result = await client.read_resource(AnyUrl("users://name/John"))
449
+ assert isinstance(name_result[0], TextResourceContents)
450
+ name_data = json.loads(name_result[0].text)
451
+ assert name_data["lookup"] == "name"
452
+ assert name_data["name"] == "John"
453
+ assert name_data["email"] == "dummy@example.com"
454
+
455
+ async def test_template_decorator_with_tags(self):
456
+ mcp = FastMCP()
457
+
458
+ @mcp.resource("resource://{param}", tags={"template", "test-tag"})
459
+ def template_resource(param: str) -> str:
460
+ return f"Template resource: {param}"
461
+
462
+ templates_dict = await mcp.get_resource_templates()
463
+ template = templates_dict["resource://{param}"]
464
+ assert template.tags == {"template", "test-tag"}
465
+
466
+ async def test_template_decorator_wildcard_param(self):
467
+ mcp = FastMCP()
468
+
469
+ @mcp.resource("resource://{param*}")
470
+ def template_resource(param: str) -> str:
471
+ return f"Template resource: {param}"
472
+
473
+ async with Client(mcp) as client:
474
+ result = await client.read_resource(AnyUrl("resource://test/data"))
475
+ assert isinstance(result[0], TextResourceContents)
476
+ assert result[0].text == "Template resource: test/data"
477
+
478
+ async def test_templates_match_in_order_of_definition(self):
479
+ """
480
+ If a wildcard template is defined first, it will take priority over another
481
+ matching template.
482
+
483
+ """
484
+ mcp = FastMCP()
485
+
486
+ @mcp.resource("resource://{param*}")
487
+ def template_resource(param: str) -> str:
488
+ return f"Template resource 1: {param}"
489
+
490
+ @mcp.resource("resource://{x}/{y}")
491
+ def template_resource_with_params(x: str, y: str) -> str:
492
+ return f"Template resource 2: {x}/{y}"
493
+
494
+ async with Client(mcp) as client:
495
+ result = await client.read_resource(AnyUrl("resource://a/b/c"))
496
+ assert isinstance(result[0], TextResourceContents)
497
+ assert result[0].text == "Template resource 1: a/b/c"
498
+
499
+ result = await client.read_resource(AnyUrl("resource://a/b"))
500
+ assert isinstance(result[0], TextResourceContents)
501
+ assert result[0].text == "Template resource 1: a/b"
502
+
503
+ async def test_templates_shadow_each_other_reorder(self):
504
+ """
505
+ If a wildcard template is defined second, it will *not* take priority over
506
+ another matching template.
507
+ """
508
+ mcp = FastMCP()
509
+
510
+ @mcp.resource("resource://{x}/{y}")
511
+ def template_resource_with_params(x: str, y: str) -> str:
512
+ return f"Template resource 1: {x}/{y}"
513
+
514
+ @mcp.resource("resource://{param*}")
515
+ def template_resource(param: str) -> str:
516
+ return f"Template resource 2: {param}"
517
+
518
+ async with Client(mcp) as client:
519
+ result = await client.read_resource(AnyUrl("resource://a/b/c"))
520
+ assert isinstance(result[0], TextResourceContents)
521
+ assert result[0].text == "Template resource 2: a/b/c"
522
+
523
+ result = await client.read_resource(AnyUrl("resource://a/b"))
524
+ assert isinstance(result[0], TextResourceContents)
525
+ assert result[0].text == "Template resource 1: a/b"
526
+
527
+
528
+ class TestContextInjection:
529
+ """Test context injection in tools."""
530
+
531
+ async def test_context_detection(self):
532
+ """Test that context parameters are properly detected."""
533
+ mcp = FastMCP()
534
+
535
+ def tool_with_context(x: int, ctx: Context) -> str:
536
+ return f"Request {ctx.request_id}: {x}"
537
+
538
+ mcp.add_tool(tool_with_context)
539
+ async with Client(mcp) as client:
540
+ tools = await client.list_tools()
541
+ assert len(tools) == 1
542
+ assert tools[0].name == "tool_with_context"
543
+
544
+ async def test_context_injection(self):
545
+ """Test that context is properly injected into tool calls."""
546
+ mcp = FastMCP()
547
+
548
+ def tool_with_context(x: int, ctx: Context) -> str:
549
+ assert ctx.request_id is not None
550
+ return f"Request {ctx.request_id}: {x}"
551
+
552
+ mcp.add_tool(tool_with_context)
553
+ async with Client(mcp) as client:
554
+ result = await client.call_tool("tool_with_context", {"x": 42})
555
+ assert len(result) == 1
556
+ content = result[0]
557
+ assert isinstance(content, TextContent)
558
+ assert "Request" in content.text
559
+ assert "42" in content.text
560
+
561
+ async def test_async_context(self):
562
+ """Test that context works in async functions."""
563
+ mcp = FastMCP()
564
+
565
+ async def async_tool(x: int, ctx: Context) -> str:
566
+ assert ctx.request_id is not None
567
+ return f"Async request {ctx.request_id}: {x}"
568
+
569
+ mcp.add_tool(async_tool)
570
+ async with Client(mcp) as client:
571
+ result = await client.call_tool("async_tool", {"x": 42})
572
+ assert len(result) == 1
573
+ content = result[0]
574
+ assert isinstance(content, TextContent)
575
+ assert "Async request" in content.text
576
+ assert "42" in content.text
577
+
578
+ async def test_context_logging(self):
579
+ from unittest.mock import patch
580
+
581
+ import mcp.server.session
582
+
583
+ """Test that context logging methods work."""
584
+ mcp = FastMCP()
585
+
586
+ async def logging_tool(msg: str, ctx: Context) -> str:
587
+ await ctx.debug("Debug message")
588
+ await ctx.info("Info message")
589
+ await ctx.warning("Warning message")
590
+ await ctx.error("Error message")
591
+ return f"Logged messages for {msg}"
592
+
593
+ mcp.add_tool(logging_tool)
594
+
595
+ with patch("mcp.server.session.ServerSession.send_log_message") as mock_log:
596
+ async with Client(mcp) as client:
597
+ result = await client.call_tool("logging_tool", {"msg": "test"})
598
+ assert len(result) == 1
599
+ content = result[0]
600
+ assert isinstance(content, TextContent)
601
+ assert "Logged messages for test" in content.text
602
+
603
+ assert mock_log.call_count == 4
604
+ mock_log.assert_any_call(
605
+ level="debug", data="Debug message", logger=None
606
+ )
607
+ mock_log.assert_any_call(level="info", data="Info message", logger=None)
608
+ mock_log.assert_any_call(
609
+ level="warning", data="Warning message", logger=None
610
+ )
611
+ mock_log.assert_any_call(
612
+ level="error", data="Error message", logger=None
613
+ )
614
+
615
+ async def test_optional_context(self):
616
+ """Test that context is optional."""
617
+ mcp = FastMCP()
618
+
619
+ def no_context(x: int) -> int:
620
+ return x * 2
621
+
622
+ mcp.add_tool(no_context)
623
+ async with Client(mcp) as client:
624
+ result = await client.call_tool("no_context", {"x": 21})
625
+ assert len(result) == 1
626
+ content = result[0]
627
+ assert isinstance(content, TextContent)
628
+ assert content.text == "42"
629
+
630
+ async def test_context_resource_access(self):
631
+ """Test that context can access resources."""
632
+ mcp = FastMCP()
633
+
634
+ @mcp.resource("test://data")
635
+ def test_resource() -> str:
636
+ return "resource data"
637
+
638
+ @mcp.tool()
639
+ async def tool_with_resource(ctx: Context) -> str:
640
+ r_iter = await ctx.read_resource("test://data")
641
+ r_list = list(r_iter)
642
+ assert len(r_list) == 1
643
+ r = r_list[0]
644
+ return f"Read resource: {r.content} with mime type {r.mime_type}"
645
+
646
+ async with Client(mcp) as client:
647
+ result = await client.call_tool("tool_with_resource", {})
648
+ assert len(result) == 1
649
+ content = result[0]
650
+ assert isinstance(content, TextContent)
651
+ assert "Read resource: resource data" in content.text
652
+
653
+ async def test_tool_decorator_with_tags(self):
654
+ """Test that the tool decorator properly sets tags."""
655
+ mcp = FastMCP()
656
+
657
+ @mcp.tool(tags={"example", "test-tag"})
658
+ def sample_tool(x: int) -> int:
659
+ return x * 2
660
+
661
+ # Verify the tool exists
662
+ async with Client(mcp) as client:
663
+ tools = await client.list_tools()
664
+ assert len(tools) == 1
665
+ # Note: MCPTool from the client API doesn't expose tags
666
+
667
+
668
+ class TestPrompts:
669
+ """Test prompt functionality in FastMCP server."""
670
+
671
+ async def test_prompt_decorator(self):
672
+ """Test that the prompt decorator registers prompts correctly."""
673
+ mcp = FastMCP()
674
+
675
+ @mcp.prompt()
676
+ def fn() -> str:
677
+ return "Hello, world!"
678
+
679
+ prompts_dict = await mcp.get_prompts()
680
+ assert len(prompts_dict) == 1
681
+ prompt = prompts_dict["fn"]
682
+ assert prompt.name == "fn"
683
+ # Don't compare functions directly since validate_call wraps them
684
+ content = await prompt.render()
685
+ assert isinstance(content[0].content, TextContent)
686
+ assert content[0].content.text == "Hello, world!"
687
+
688
+ async def test_prompt_decorator_with_name(self):
689
+ """Test prompt decorator with custom name."""
690
+ mcp = FastMCP()
691
+
692
+ @mcp.prompt(name="custom_name")
693
+ def fn() -> str:
694
+ return "Hello, world!"
695
+
696
+ prompts_dict = await mcp.get_prompts()
697
+ assert len(prompts_dict) == 1
698
+ prompt = prompts_dict["custom_name"]
699
+ assert prompt.name == "custom_name"
700
+ content = await prompt.render()
701
+ assert isinstance(content[0].content, TextContent)
702
+ assert content[0].content.text == "Hello, world!"
703
+
704
+ async def test_prompt_decorator_with_description(self):
705
+ """Test prompt decorator with custom description."""
706
+ mcp = FastMCP()
707
+
708
+ @mcp.prompt(description="A custom description")
709
+ def fn() -> str:
710
+ return "Hello, world!"
711
+
712
+ prompts_dict = await mcp.get_prompts()
713
+ assert len(prompts_dict) == 1
714
+ prompt = prompts_dict["fn"]
715
+ assert prompt.description == "A custom description"
716
+ content = await prompt.render()
717
+ assert isinstance(content[0].content, TextContent)
718
+ assert content[0].content.text == "Hello, world!"
719
+
720
+ def test_prompt_decorator_error(self):
721
+ """Test error when decorator is used incorrectly."""
722
+ mcp = FastMCP()
723
+ with pytest.raises(TypeError, match="decorator was used incorrectly"):
724
+
725
+ @mcp.prompt # type: ignore
726
+ def fn() -> str:
727
+ return "Hello, world!"
728
+
729
+ async def test_list_prompts(self):
730
+ """Test listing prompts through MCP protocol."""
731
+ mcp = FastMCP()
732
+
733
+ @mcp.prompt()
734
+ def fn(name: str, optional: str = "default") -> str:
735
+ return f"Hello, {name}! {optional}"
736
+
737
+ prompts_dict = await mcp.get_prompts()
738
+ assert len(prompts_dict) == 1
739
+
740
+ async with Client(mcp) as client:
741
+ prompts = await client.list_prompts()
742
+ assert len(prompts) == 1
743
+ assert prompts[0].name == "fn"
744
+ assert prompts[0].description is None
745
+ assert prompts[0].arguments is not None
746
+ assert len(prompts[0].arguments) == 2
747
+ assert prompts[0].arguments[0].name == "name"
748
+ assert prompts[0].arguments[0].required is True
749
+ assert prompts[0].arguments[1].name == "optional"
750
+ assert prompts[0].arguments[1].required is False
751
+
752
+ async def test_get_prompt(self):
753
+ """Test getting a prompt through MCP protocol."""
754
+ mcp = FastMCP()
755
+
756
+ @mcp.prompt()
757
+ def fn(name: str) -> str:
758
+ return f"Hello, {name}!"
759
+
760
+ async with Client(mcp) as client:
761
+ result = await client.get_prompt("fn", {"name": "World"})
762
+ assert len(result) == 1
763
+ message = result[0]
764
+ assert message.role == "user"
765
+ content = message.content
766
+ assert isinstance(content, TextContent)
767
+ assert content.text == "Hello, World!"
768
+
769
+ async def test_get_prompt_with_resource(self):
770
+ """Test getting a prompt that returns resource content."""
771
+ mcp = FastMCP()
772
+
773
+ @mcp.prompt()
774
+ def fn() -> Message:
775
+ return UserMessage(
776
+ content=EmbeddedResource(
777
+ type="resource",
778
+ resource=TextResourceContents(
779
+ uri=AnyUrl("file://file.txt"),
780
+ text="File contents",
781
+ mimeType="text/plain",
782
+ ),
783
+ )
784
+ )
785
+
786
+ async with Client(mcp) as client:
787
+ result = await client.get_prompt("fn")
788
+ assert result[0].role == "user"
789
+ content = result[0].content
790
+ assert isinstance(content, EmbeddedResource)
791
+ resource = content.resource
792
+ assert isinstance(resource, TextResourceContents)
793
+ assert resource.text == "File contents"
794
+ assert resource.mimeType == "text/plain"
795
+
796
+ async def test_get_unknown_prompt(self):
797
+ """Test error when getting unknown prompt."""
798
+ mcp = FastMCP()
799
+ with pytest.raises(ClientError, match="Unknown prompt"):
800
+ async with Client(mcp) as client:
801
+ await client.get_prompt("unknown")
802
+
803
+ async def test_get_prompt_missing_args(self):
804
+ """Test error when required arguments are missing."""
805
+ mcp = FastMCP()
806
+
807
+ @mcp.prompt()
808
+ def prompt_fn(name: str) -> str:
809
+ return f"Hello, {name}!"
810
+
811
+ with pytest.raises(ClientError, match="Missing required arguments"):
812
+ async with Client(mcp) as client:
813
+ await client.get_prompt("prompt_fn")
814
+
815
+ async def test_resource_decorator_with_tags(self):
816
+ """Test that the resource decorator supports tags."""
817
+ mcp = FastMCP()
818
+
819
+ @mcp.resource("resource://data", tags={"example", "test-tag"})
820
+ def get_data() -> str:
821
+ return "Hello, world!"
822
+
823
+ resources_dict = await mcp.get_resources()
824
+ resources = list(resources_dict.values())
825
+ assert len(resources) == 1
826
+ assert resources[0].tags == {"example", "test-tag"}
827
+
828
+ async def test_template_decorator_with_tags(self):
829
+ """Test that the template decorator properly sets tags."""
830
+ mcp = FastMCP()
831
+
832
+ @mcp.resource("resource://{param}", tags={"template", "test-tag"})
833
+ def template_resource(param: str) -> str:
834
+ return f"Template resource: {param}"
835
+
836
+ templates_dict = await mcp.get_resource_templates()
837
+ template = templates_dict["resource://{param}"]
838
+ assert template.tags == {"template", "test-tag"}
839
+
840
+ async def test_prompt_decorator_with_tags(self):
841
+ """Test that the prompt decorator properly sets tags."""
842
+ mcp = FastMCP()
843
+
844
+ @mcp.prompt(tags={"example", "test-tag"})
845
+ def sample_prompt() -> str:
846
+ return "Hello, world!"
847
+
848
+ prompts_dict = await mcp.get_prompts()
849
+ assert len(prompts_dict) == 1
850
+ prompt = prompts_dict["sample_prompt"]
851
+ assert prompt.tags == {"example", "test-tag"}