Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
d4fceae
1
Parent(s): 0c34dfa
Update test typing
Browse files- tests/client/test_client.py +8 -14
- tests/client/test_openapi.py +9 -19
- tests/client/test_roots.py +1 -3
- tests/client/test_sse.py +1 -3
- tests/client/test_stdio.py +9 -19
- tests/client/test_streamable_http.py +1 -3
- tests/prompts/test_prompt_manager.py +2 -4
- tests/resources/test_file_resources.py +0 -1
- tests/server/http/test_http_dependencies.py +6 -13
- tests/server/openapi/test_openapi.py +16 -38
- tests/server/test_import_server.py +5 -13
- tests/server/test_mount.py +12 -26
- tests/server/test_proxy.py +7 -17
- tests/server/test_server.py +37 -78
- tests/server/test_server_interactions.py +59 -111
- tests/server/test_tool_annotations.py +3 -4
- tests/test_examples.py +8 -26
- tests/tools/test_tool.py +14 -32
- tests/tools/test_tool_manager.py +21 -90
- tests/utilities/test_mcp_config.py +2 -6
tests/client/test_client.py
CHANGED
|
@@ -16,7 +16,6 @@ from fastmcp.client.transports import (
|
|
| 16 |
infer_transport,
|
| 17 |
)
|
| 18 |
from fastmcp.exceptions import ResourceError, ToolError
|
| 19 |
-
from fastmcp.prompts.prompt import TextContent
|
| 20 |
from fastmcp.server.server import FastMCP
|
| 21 |
|
| 22 |
|
|
@@ -201,8 +200,7 @@ async def test_get_prompt(fastmcp_server):
|
|
| 201 |
result = await client.get_prompt("welcome", {"name": "Developer"})
|
| 202 |
|
| 203 |
# The result should contain our welcome message
|
| 204 |
-
assert
|
| 205 |
-
assert result.messages[0].content.text == "Welcome to FastMCP, Developer!"
|
| 206 |
assert result.description == "Example greeting prompt."
|
| 207 |
|
| 208 |
|
|
@@ -214,8 +212,7 @@ async def test_get_prompt_mcp(fastmcp_server):
|
|
| 214 |
result = await client.get_prompt_mcp("welcome", {"name": "Developer"})
|
| 215 |
|
| 216 |
# The result should contain our welcome message
|
| 217 |
-
assert
|
| 218 |
-
assert result.messages[0].content.text == "Welcome to FastMCP, Developer!"
|
| 219 |
assert result.description == "Example greeting prompt."
|
| 220 |
|
| 221 |
|
|
@@ -522,9 +519,8 @@ class TestErrorHandling:
|
|
| 522 |
async with client:
|
| 523 |
result = await client.call_tool_mcp("error_tool", {})
|
| 524 |
assert result.isError
|
| 525 |
-
assert
|
| 526 |
-
assert "
|
| 527 |
-
assert "abc" in result.content[0].text
|
| 528 |
|
| 529 |
async def test_general_tool_exceptions_are_masked_when_enabled(self):
|
| 530 |
mcp = FastMCP("TestServer", mask_error_details=True)
|
|
@@ -538,9 +534,8 @@ class TestErrorHandling:
|
|
| 538 |
async with client:
|
| 539 |
result = await client.call_tool_mcp("error_tool", {})
|
| 540 |
assert result.isError
|
| 541 |
-
assert
|
| 542 |
-
assert "
|
| 543 |
-
assert "abc" not in result.content[0].text
|
| 544 |
|
| 545 |
async def test_specific_tool_errors_are_sent_to_client(self):
|
| 546 |
mcp = FastMCP("TestServer")
|
|
@@ -554,9 +549,8 @@ class TestErrorHandling:
|
|
| 554 |
async with client:
|
| 555 |
result = await client.call_tool_mcp("custom_error_tool", {})
|
| 556 |
assert result.isError
|
| 557 |
-
assert
|
| 558 |
-
assert "
|
| 559 |
-
assert "abc" in result.content[0].text
|
| 560 |
|
| 561 |
async def test_general_resource_exceptions_are_not_masked_by_default(self):
|
| 562 |
mcp = FastMCP("TestServer")
|
|
|
|
| 16 |
infer_transport,
|
| 17 |
)
|
| 18 |
from fastmcp.exceptions import ResourceError, ToolError
|
|
|
|
| 19 |
from fastmcp.server.server import FastMCP
|
| 20 |
|
| 21 |
|
|
|
|
| 200 |
result = await client.get_prompt("welcome", {"name": "Developer"})
|
| 201 |
|
| 202 |
# The result should contain our welcome message
|
| 203 |
+
assert result.messages[0].content.text == "Welcome to FastMCP, Developer!" # type: ignore[attr-defined]
|
|
|
|
| 204 |
assert result.description == "Example greeting prompt."
|
| 205 |
|
| 206 |
|
|
|
|
| 212 |
result = await client.get_prompt_mcp("welcome", {"name": "Developer"})
|
| 213 |
|
| 214 |
# The result should contain our welcome message
|
| 215 |
+
assert result.messages[0].content.text == "Welcome to FastMCP, Developer!" # type: ignore[attr-defined]
|
|
|
|
| 216 |
assert result.description == "Example greeting prompt."
|
| 217 |
|
| 218 |
|
|
|
|
| 519 |
async with client:
|
| 520 |
result = await client.call_tool_mcp("error_tool", {})
|
| 521 |
assert result.isError
|
| 522 |
+
assert "test error" in result.content[0].text # type: ignore[attr-defined]
|
| 523 |
+
assert "abc" in result.content[0].text # type: ignore[attr-defined]
|
|
|
|
| 524 |
|
| 525 |
async def test_general_tool_exceptions_are_masked_when_enabled(self):
|
| 526 |
mcp = FastMCP("TestServer", mask_error_details=True)
|
|
|
|
| 534 |
async with client:
|
| 535 |
result = await client.call_tool_mcp("error_tool", {})
|
| 536 |
assert result.isError
|
| 537 |
+
assert "test error" not in result.content[0].text # type: ignore[attr-defined]
|
| 538 |
+
assert "abc" not in result.content[0].text # type: ignore[attr-defined]
|
|
|
|
| 539 |
|
| 540 |
async def test_specific_tool_errors_are_sent_to_client(self):
|
| 541 |
mcp = FastMCP("TestServer")
|
|
|
|
| 549 |
async with client:
|
| 550 |
result = await client.call_tool_mcp("custom_error_tool", {})
|
| 551 |
assert result.isError
|
| 552 |
+
assert "test error" in result.content[0].text # type: ignore[attr-defined]
|
| 553 |
+
assert "abc" in result.content[0].text # type: ignore[attr-defined]
|
|
|
|
| 554 |
|
| 555 |
async def test_general_resource_exceptions_are_not_masked_by_default(self):
|
| 556 |
mcp = FastMCP("TestServer")
|
tests/client/test_openapi.py
CHANGED
|
@@ -5,7 +5,6 @@ from collections.abc import Generator
|
|
| 5 |
import pytest
|
| 6 |
import uvicorn
|
| 7 |
from fastapi import FastAPI, Request
|
| 8 |
-
from mcp.types import TextContent, TextResourceContents
|
| 9 |
|
| 10 |
from fastmcp import Client, FastMCP
|
| 11 |
from fastmcp.client.transports import SSETransport, StreamableHttpTransport
|
|
@@ -111,8 +110,7 @@ class TestClientHeaders:
|
|
| 111 |
transport=SSETransport(sse_server, headers={"X-TEST": "test-123"})
|
| 112 |
) as client:
|
| 113 |
result = await client.read_resource("resource://get_headers_headers_get")
|
| 114 |
-
|
| 115 |
-
headers = json.loads(result[0].text)
|
| 116 |
assert headers["x-test"] == "test-123"
|
| 117 |
|
| 118 |
async def test_client_headers_shttp_resource(self, shttp_server: str):
|
|
@@ -122,8 +120,7 @@ class TestClientHeaders:
|
|
| 122 |
)
|
| 123 |
) as client:
|
| 124 |
result = await client.read_resource("resource://get_headers_headers_get")
|
| 125 |
-
|
| 126 |
-
headers = json.loads(result[0].text)
|
| 127 |
assert headers["x-test"] == "test-123"
|
| 128 |
|
| 129 |
async def test_client_headers_sse_resource_template(self, sse_server: str):
|
|
@@ -133,8 +130,7 @@ class TestClientHeaders:
|
|
| 133 |
result = await client.read_resource(
|
| 134 |
"resource://get_header_by_name_headers/x-test"
|
| 135 |
)
|
| 136 |
-
|
| 137 |
-
header = json.loads(result[0].text)
|
| 138 |
assert header == "test-123"
|
| 139 |
|
| 140 |
async def test_client_headers_shttp_resource_template(self, shttp_server: str):
|
|
@@ -146,8 +142,7 @@ class TestClientHeaders:
|
|
| 146 |
result = await client.read_resource(
|
| 147 |
"resource://get_header_by_name_headers/x-test"
|
| 148 |
)
|
| 149 |
-
|
| 150 |
-
header = json.loads(result[0].text)
|
| 151 |
assert header == "test-123"
|
| 152 |
|
| 153 |
async def test_client_headers_sse_tool(self, sse_server: str):
|
|
@@ -155,8 +150,7 @@ class TestClientHeaders:
|
|
| 155 |
transport=SSETransport(sse_server, headers={"X-TEST": "test-123"})
|
| 156 |
) as client:
|
| 157 |
result = await client.call_tool("post_headers_headers_post")
|
| 158 |
-
|
| 159 |
-
headers = json.loads(result[0].text)
|
| 160 |
assert headers["x-test"] == "test-123"
|
| 161 |
|
| 162 |
async def test_client_headers_shttp_tool(self, shttp_server: str):
|
|
@@ -166,8 +160,7 @@ class TestClientHeaders:
|
|
| 166 |
)
|
| 167 |
) as client:
|
| 168 |
result = await client.call_tool("post_headers_headers_post")
|
| 169 |
-
|
| 170 |
-
headers = json.loads(result[0].text)
|
| 171 |
assert headers["x-test"] == "test-123"
|
| 172 |
|
| 173 |
async def test_client_overrides_server_headers(self, shttp_server: str):
|
|
@@ -177,8 +170,7 @@ class TestClientHeaders:
|
|
| 177 |
)
|
| 178 |
) as client:
|
| 179 |
result = await client.read_resource("resource://get_headers_headers_get")
|
| 180 |
-
|
| 181 |
-
headers = json.loads(result[0].text)
|
| 182 |
assert headers["x-server-header"] == "test-client"
|
| 183 |
|
| 184 |
async def test_client_with_excluded_header_is_ignored(self, sse_server: str):
|
|
@@ -193,8 +185,7 @@ class TestClientHeaders:
|
|
| 193 |
)
|
| 194 |
) as client:
|
| 195 |
result = await client.read_resource("resource://get_headers_headers_get")
|
| 196 |
-
|
| 197 |
-
headers = json.loads(result[0].text)
|
| 198 |
assert headers["not-host"] == "1.2.3.4"
|
| 199 |
assert headers["host"] == "fastapi"
|
| 200 |
|
|
@@ -204,6 +195,5 @@ class TestClientHeaders:
|
|
| 204 |
"""
|
| 205 |
async with Client(transport=StreamableHttpTransport(proxy_server)) as client:
|
| 206 |
result = await client.read_resource("resource://get_headers_headers_get")
|
| 207 |
-
|
| 208 |
-
headers = json.loads(result[0].text)
|
| 209 |
assert headers["x-server-header"] == "test-abc"
|
|
|
|
| 5 |
import pytest
|
| 6 |
import uvicorn
|
| 7 |
from fastapi import FastAPI, Request
|
|
|
|
| 8 |
|
| 9 |
from fastmcp import Client, FastMCP
|
| 10 |
from fastmcp.client.transports import SSETransport, StreamableHttpTransport
|
|
|
|
| 110 |
transport=SSETransport(sse_server, headers={"X-TEST": "test-123"})
|
| 111 |
) as client:
|
| 112 |
result = await client.read_resource("resource://get_headers_headers_get")
|
| 113 |
+
headers = json.loads(result[0].text) # type: ignore[attr-defined]
|
|
|
|
| 114 |
assert headers["x-test"] == "test-123"
|
| 115 |
|
| 116 |
async def test_client_headers_shttp_resource(self, shttp_server: str):
|
|
|
|
| 120 |
)
|
| 121 |
) as client:
|
| 122 |
result = await client.read_resource("resource://get_headers_headers_get")
|
| 123 |
+
headers = json.loads(result[0].text) # type: ignore[attr-defined]
|
|
|
|
| 124 |
assert headers["x-test"] == "test-123"
|
| 125 |
|
| 126 |
async def test_client_headers_sse_resource_template(self, sse_server: str):
|
|
|
|
| 130 |
result = await client.read_resource(
|
| 131 |
"resource://get_header_by_name_headers/x-test"
|
| 132 |
)
|
| 133 |
+
header = json.loads(result[0].text) # type: ignore[attr-defined]
|
|
|
|
| 134 |
assert header == "test-123"
|
| 135 |
|
| 136 |
async def test_client_headers_shttp_resource_template(self, shttp_server: str):
|
|
|
|
| 142 |
result = await client.read_resource(
|
| 143 |
"resource://get_header_by_name_headers/x-test"
|
| 144 |
)
|
| 145 |
+
header = json.loads(result[0].text) # type: ignore[attr-defined]
|
|
|
|
| 146 |
assert header == "test-123"
|
| 147 |
|
| 148 |
async def test_client_headers_sse_tool(self, sse_server: str):
|
|
|
|
| 150 |
transport=SSETransport(sse_server, headers={"X-TEST": "test-123"})
|
| 151 |
) as client:
|
| 152 |
result = await client.call_tool("post_headers_headers_post")
|
| 153 |
+
headers = json.loads(result[0].text) # type: ignore[attr-defined]
|
|
|
|
| 154 |
assert headers["x-test"] == "test-123"
|
| 155 |
|
| 156 |
async def test_client_headers_shttp_tool(self, shttp_server: str):
|
|
|
|
| 160 |
)
|
| 161 |
) as client:
|
| 162 |
result = await client.call_tool("post_headers_headers_post")
|
| 163 |
+
headers = json.loads(result[0].text) # type: ignore[attr-defined]
|
|
|
|
| 164 |
assert headers["x-test"] == "test-123"
|
| 165 |
|
| 166 |
async def test_client_overrides_server_headers(self, shttp_server: str):
|
|
|
|
| 170 |
)
|
| 171 |
) as client:
|
| 172 |
result = await client.read_resource("resource://get_headers_headers_get")
|
| 173 |
+
headers = json.loads(result[0].text) # type: ignore[attr-defined]
|
|
|
|
| 174 |
assert headers["x-server-header"] == "test-client"
|
| 175 |
|
| 176 |
async def test_client_with_excluded_header_is_ignored(self, sse_server: str):
|
|
|
|
| 185 |
)
|
| 186 |
) as client:
|
| 187 |
result = await client.read_resource("resource://get_headers_headers_get")
|
| 188 |
+
headers = json.loads(result[0].text) # type: ignore[attr-defined]
|
|
|
|
| 189 |
assert headers["not-host"] == "1.2.3.4"
|
| 190 |
assert headers["host"] == "fastapi"
|
| 191 |
|
|
|
|
| 195 |
"""
|
| 196 |
async with Client(transport=StreamableHttpTransport(proxy_server)) as client:
|
| 197 |
result = await client.read_resource("resource://get_headers_headers_get")
|
| 198 |
+
headers = json.loads(result[0].text) # type: ignore[attr-defined]
|
|
|
|
| 199 |
assert headers["x-server-header"] == "test-abc"
|
tests/client/test_roots.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
import json
|
| 2 |
|
| 3 |
import pytest
|
| 4 |
-
from mcp.types import TextContent
|
| 5 |
|
| 6 |
from fastmcp import Client, Context, FastMCP
|
| 7 |
|
|
@@ -41,8 +40,7 @@ class TestClientRoots:
|
|
| 41 |
async def test_valid_roots(self, fastmcp_server: FastMCP, roots: list[str]):
|
| 42 |
async with Client(fastmcp_server, roots=roots) as client:
|
| 43 |
result = await client.call_tool("list_roots", {})
|
| 44 |
-
assert
|
| 45 |
-
assert json.loads(result[0].text) == [
|
| 46 |
"file://x/y/z",
|
| 47 |
"file://x/y/z",
|
| 48 |
]
|
|
|
|
| 1 |
import json
|
| 2 |
|
| 3 |
import pytest
|
|
|
|
| 4 |
|
| 5 |
from fastmcp import Client, Context, FastMCP
|
| 6 |
|
|
|
|
| 40 |
async def test_valid_roots(self, fastmcp_server: FastMCP, roots: list[str]):
|
| 41 |
async with Client(fastmcp_server, roots=roots) as client:
|
| 42 |
result = await client.call_tool("list_roots", {})
|
| 43 |
+
assert json.loads(result[0].text) == [ # type: ignore[attr-defined]
|
|
|
|
| 44 |
"file://x/y/z",
|
| 45 |
"file://x/y/z",
|
| 46 |
]
|
tests/client/test_sse.py
CHANGED
|
@@ -6,7 +6,6 @@ from collections.abc import Generator
|
|
| 6 |
import pytest
|
| 7 |
import uvicorn
|
| 8 |
from mcp import McpError
|
| 9 |
-
from mcp.types import TextResourceContents
|
| 10 |
from starlette.applications import Starlette
|
| 11 |
from starlette.routing import Mount
|
| 12 |
|
|
@@ -96,8 +95,7 @@ async def test_http_headers(sse_server: str):
|
|
| 96 |
transport=SSETransport(sse_server, headers={"X-DEMO-HEADER": "ABC"})
|
| 97 |
) as client:
|
| 98 |
raw_result = await client.read_resource("request://headers")
|
| 99 |
-
|
| 100 |
-
json_result = json.loads(raw_result[0].text)
|
| 101 |
assert "x-demo-header" in json_result
|
| 102 |
assert json_result["x-demo-header"] == "ABC"
|
| 103 |
|
|
|
|
| 6 |
import pytest
|
| 7 |
import uvicorn
|
| 8 |
from mcp import McpError
|
|
|
|
| 9 |
from starlette.applications import Starlette
|
| 10 |
from starlette.routing import Mount
|
| 11 |
|
|
|
|
| 95 |
transport=SSETransport(sse_server, headers={"X-DEMO-HEADER": "ABC"})
|
| 96 |
) as client:
|
| 97 |
raw_result = await client.read_resource("request://headers")
|
| 98 |
+
json_result = json.loads(raw_result[0].text) # type: ignore[attr-defined]
|
|
|
|
| 99 |
assert "x-demo-header" in json_result
|
| 100 |
assert json_result["x-demo-header"] == "ABC"
|
| 101 |
|
tests/client/test_stdio.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
import inspect
|
| 2 |
|
| 3 |
import pytest
|
| 4 |
-
from mcp.types import TextContent
|
| 5 |
|
| 6 |
from fastmcp import Client
|
| 7 |
from fastmcp.client.transports import PythonStdioTransport, StdioTransport
|
|
@@ -49,13 +48,11 @@ class TestKeepAlive:
|
|
| 49 |
|
| 50 |
async with client:
|
| 51 |
result1 = await client.call_tool("pid")
|
| 52 |
-
|
| 53 |
-
pid1 = int(result1[0].text)
|
| 54 |
|
| 55 |
async with client:
|
| 56 |
result2 = await client.call_tool("pid")
|
| 57 |
-
|
| 58 |
-
pid2 = int(result2[0].text)
|
| 59 |
|
| 60 |
assert pid1 == pid2
|
| 61 |
|
|
@@ -69,13 +66,11 @@ class TestKeepAlive:
|
|
| 69 |
|
| 70 |
async with client:
|
| 71 |
result1 = await client.call_tool("pid")
|
| 72 |
-
|
| 73 |
-
pid1 = int(result1[0].text)
|
| 74 |
|
| 75 |
async with client:
|
| 76 |
result2 = await client.call_tool("pid")
|
| 77 |
-
|
| 78 |
-
pid2 = int(result2[0].text)
|
| 79 |
|
| 80 |
assert pid1 != pid2
|
| 81 |
|
|
@@ -85,15 +80,13 @@ class TestKeepAlive:
|
|
| 85 |
|
| 86 |
async with client:
|
| 87 |
result1 = await client.call_tool("pid")
|
| 88 |
-
|
| 89 |
-
pid1 = int(result1[0].text)
|
| 90 |
|
| 91 |
await client.close()
|
| 92 |
|
| 93 |
async with client:
|
| 94 |
result2 = await client.call_tool("pid")
|
| 95 |
-
|
| 96 |
-
pid2 = int(result2[0].text)
|
| 97 |
|
| 98 |
assert pid1 != pid2
|
| 99 |
|
|
@@ -103,17 +96,14 @@ class TestKeepAlive:
|
|
| 103 |
|
| 104 |
async with client:
|
| 105 |
result1 = await client.call_tool("pid")
|
| 106 |
-
|
| 107 |
-
pid1 = int(result1[0].text)
|
| 108 |
|
| 109 |
async with client:
|
| 110 |
result2 = await client.call_tool("pid")
|
| 111 |
-
|
| 112 |
-
pid2 = int(result2[0].text)
|
| 113 |
|
| 114 |
result3 = await client.call_tool("pid")
|
| 115 |
-
|
| 116 |
-
pid3 = int(result3[0].text)
|
| 117 |
|
| 118 |
assert pid1 == pid2 == pid3
|
| 119 |
|
|
|
|
| 1 |
import inspect
|
| 2 |
|
| 3 |
import pytest
|
|
|
|
| 4 |
|
| 5 |
from fastmcp import Client
|
| 6 |
from fastmcp.client.transports import PythonStdioTransport, StdioTransport
|
|
|
|
| 48 |
|
| 49 |
async with client:
|
| 50 |
result1 = await client.call_tool("pid")
|
| 51 |
+
pid1 = int(result1[0].text) # type: ignore[attr-defined]
|
|
|
|
| 52 |
|
| 53 |
async with client:
|
| 54 |
result2 = await client.call_tool("pid")
|
| 55 |
+
pid2 = int(result2[0].text) # type: ignore[attr-defined]
|
|
|
|
| 56 |
|
| 57 |
assert pid1 == pid2
|
| 58 |
|
|
|
|
| 66 |
|
| 67 |
async with client:
|
| 68 |
result1 = await client.call_tool("pid")
|
| 69 |
+
pid1 = int(result1[0].text) # type: ignore[attr-defined]
|
|
|
|
| 70 |
|
| 71 |
async with client:
|
| 72 |
result2 = await client.call_tool("pid")
|
| 73 |
+
pid2 = int(result2[0].text) # type: ignore[attr-defined]
|
|
|
|
| 74 |
|
| 75 |
assert pid1 != pid2
|
| 76 |
|
|
|
|
| 80 |
|
| 81 |
async with client:
|
| 82 |
result1 = await client.call_tool("pid")
|
| 83 |
+
pid1 = int(result1[0].text) # type: ignore[attr-defined]
|
|
|
|
| 84 |
|
| 85 |
await client.close()
|
| 86 |
|
| 87 |
async with client:
|
| 88 |
result2 = await client.call_tool("pid")
|
| 89 |
+
pid2 = int(result2[0].text) # type: ignore[attr-defined]
|
|
|
|
| 90 |
|
| 91 |
assert pid1 != pid2
|
| 92 |
|
|
|
|
| 96 |
|
| 97 |
async with client:
|
| 98 |
result1 = await client.call_tool("pid")
|
| 99 |
+
pid1 = int(result1[0].text) # type: ignore[attr-defined]
|
|
|
|
| 100 |
|
| 101 |
async with client:
|
| 102 |
result2 = await client.call_tool("pid")
|
| 103 |
+
pid2 = int(result2[0].text) # type: ignore[attr-defined]
|
|
|
|
| 104 |
|
| 105 |
result3 = await client.call_tool("pid")
|
| 106 |
+
pid3 = int(result3[0].text) # type: ignore[attr-defined]
|
|
|
|
| 107 |
|
| 108 |
assert pid1 == pid2 == pid3
|
| 109 |
|
tests/client/test_streamable_http.py
CHANGED
|
@@ -6,7 +6,6 @@ from collections.abc import Generator
|
|
| 6 |
import pytest
|
| 7 |
import uvicorn
|
| 8 |
from mcp import McpError
|
| 9 |
-
from mcp.types import TextResourceContents
|
| 10 |
from starlette.applications import Starlette
|
| 11 |
from starlette.routing import Mount
|
| 12 |
|
|
@@ -106,8 +105,7 @@ async def test_http_headers(streamable_http_server: str):
|
|
| 106 |
)
|
| 107 |
) as client:
|
| 108 |
raw_result = await client.read_resource("request://headers")
|
| 109 |
-
|
| 110 |
-
json_result = json.loads(raw_result[0].text)
|
| 111 |
assert "x-demo-header" in json_result
|
| 112 |
assert json_result["x-demo-header"] == "ABC"
|
| 113 |
|
|
|
|
| 6 |
import pytest
|
| 7 |
import uvicorn
|
| 8 |
from mcp import McpError
|
|
|
|
| 9 |
from starlette.applications import Starlette
|
| 10 |
from starlette.routing import Mount
|
| 11 |
|
|
|
|
| 105 |
)
|
| 106 |
) as client:
|
| 107 |
raw_result = await client.read_resource("request://headers")
|
| 108 |
+
json_result = json.loads(raw_result[0].text) # type: ignore[attr-defined]
|
|
|
|
| 109 |
assert "x-demo-header" in json_result
|
| 110 |
assert json_result["x-demo-header"] == "ABC"
|
| 111 |
|
tests/prompts/test_prompt_manager.py
CHANGED
|
@@ -393,8 +393,7 @@ class TestContextHandling:
|
|
| 393 |
messages = await prompt.render(arguments={"x": 42})
|
| 394 |
|
| 395 |
assert len(messages) == 1
|
| 396 |
-
assert
|
| 397 |
-
assert messages[0].content.text == "42"
|
| 398 |
|
| 399 |
async def test_context_optional(self):
|
| 400 |
"""Test that context is optional when rendering prompts."""
|
|
@@ -416,8 +415,7 @@ class TestContextHandling:
|
|
| 416 |
)
|
| 417 |
|
| 418 |
assert len(messages) == 1
|
| 419 |
-
assert
|
| 420 |
-
assert messages[0].content.text == "42"
|
| 421 |
|
| 422 |
async def test_annotated_context_parameter_detection(self):
|
| 423 |
"""Test that annotated context parameters are properly detected in
|
|
|
|
| 393 |
messages = await prompt.render(arguments={"x": 42})
|
| 394 |
|
| 395 |
assert len(messages) == 1
|
| 396 |
+
assert messages[0].content.text == "42" # type: ignore[attr-defined]
|
|
|
|
| 397 |
|
| 398 |
async def test_context_optional(self):
|
| 399 |
"""Test that context is optional when rendering prompts."""
|
|
|
|
| 415 |
)
|
| 416 |
|
| 417 |
assert len(messages) == 1
|
| 418 |
+
assert messages[0].content.text == "42" # type: ignore[attr-defined]
|
|
|
|
| 419 |
|
| 420 |
async def test_annotated_context_parameter_detection(self):
|
| 421 |
"""Test that annotated context parameters are properly detected in
|
tests/resources/test_file_resources.py
CHANGED
|
@@ -74,7 +74,6 @@ class TestFileResource:
|
|
| 74 |
is_binary=True,
|
| 75 |
)
|
| 76 |
content = await resource.read()
|
| 77 |
-
assert isinstance(content, bytes)
|
| 78 |
assert content == b"test content"
|
| 79 |
|
| 80 |
def test_relative_path_error(self):
|
|
|
|
| 74 |
is_binary=True,
|
| 75 |
)
|
| 76 |
content = await resource.read()
|
|
|
|
| 77 |
assert content == b"test content"
|
| 78 |
|
| 79 |
def test_relative_path_error(self):
|
tests/server/http/test_http_dependencies.py
CHANGED
|
@@ -4,7 +4,6 @@ from collections.abc import Generator
|
|
| 4 |
|
| 5 |
import pytest
|
| 6 |
import uvicorn
|
| 7 |
-
from mcp.types import TextContent, TextResourceContents
|
| 8 |
|
| 9 |
from fastmcp.client import Client
|
| 10 |
from fastmcp.client.transports import SSETransport, StreamableHttpTransport
|
|
@@ -99,8 +98,7 @@ async def test_http_headers_resource_shttp(shttp_server: str):
|
|
| 99 |
)
|
| 100 |
) as client:
|
| 101 |
raw_result = await client.read_resource("request://headers")
|
| 102 |
-
|
| 103 |
-
json_result = json.loads(raw_result[0].text)
|
| 104 |
assert "x-demo-header" in json_result
|
| 105 |
assert json_result["x-demo-header"] == "ABC"
|
| 106 |
|
|
@@ -111,8 +109,7 @@ async def test_http_headers_resource_sse(sse_server: str):
|
|
| 111 |
transport=SSETransport(sse_server, headers={"X-DEMO-HEADER": "ABC"})
|
| 112 |
) as client:
|
| 113 |
raw_result = await client.read_resource("request://headers")
|
| 114 |
-
|
| 115 |
-
json_result = json.loads(raw_result[0].text)
|
| 116 |
assert "x-demo-header" in json_result
|
| 117 |
assert json_result["x-demo-header"] == "ABC"
|
| 118 |
|
|
@@ -125,8 +122,7 @@ async def test_http_headers_tool_shttp(shttp_server: str):
|
|
| 125 |
)
|
| 126 |
) as client:
|
| 127 |
result = await client.call_tool("get_headers_tool")
|
| 128 |
-
|
| 129 |
-
json_result = json.loads(result[0].text)
|
| 130 |
assert "x-demo-header" in json_result
|
| 131 |
assert json_result["x-demo-header"] == "ABC"
|
| 132 |
|
|
@@ -136,8 +132,7 @@ async def test_http_headers_tool_sse(sse_server: str):
|
|
| 136 |
transport=SSETransport(sse_server, headers={"X-DEMO-HEADER": "ABC"})
|
| 137 |
) as client:
|
| 138 |
result = await client.call_tool("get_headers_tool")
|
| 139 |
-
|
| 140 |
-
json_result = json.loads(result[0].text)
|
| 141 |
assert "x-demo-header" in json_result
|
| 142 |
assert json_result["x-demo-header"] == "ABC"
|
| 143 |
|
|
@@ -150,8 +145,7 @@ async def test_http_headers_prompt_shttp(shttp_server: str):
|
|
| 150 |
)
|
| 151 |
) as client:
|
| 152 |
result = await client.get_prompt("get_headers_prompt")
|
| 153 |
-
|
| 154 |
-
json_result = json.loads(result.messages[0].content.text)
|
| 155 |
assert "x-demo-header" in json_result
|
| 156 |
assert json_result["x-demo-header"] == "ABC"
|
| 157 |
|
|
@@ -162,7 +156,6 @@ async def test_http_headers_prompt_sse(sse_server: str):
|
|
| 162 |
transport=SSETransport(sse_server, headers={"X-DEMO-HEADER": "ABC"})
|
| 163 |
) as client:
|
| 164 |
result = await client.get_prompt("get_headers_prompt")
|
| 165 |
-
|
| 166 |
-
json_result = json.loads(result.messages[0].content.text)
|
| 167 |
assert "x-demo-header" in json_result
|
| 168 |
assert json_result["x-demo-header"] == "ABC"
|
|
|
|
| 4 |
|
| 5 |
import pytest
|
| 6 |
import uvicorn
|
|
|
|
| 7 |
|
| 8 |
from fastmcp.client import Client
|
| 9 |
from fastmcp.client.transports import SSETransport, StreamableHttpTransport
|
|
|
|
| 98 |
)
|
| 99 |
) as client:
|
| 100 |
raw_result = await client.read_resource("request://headers")
|
| 101 |
+
json_result = json.loads(raw_result[0].text) # type: ignore[attr-defined]
|
|
|
|
| 102 |
assert "x-demo-header" in json_result
|
| 103 |
assert json_result["x-demo-header"] == "ABC"
|
| 104 |
|
|
|
|
| 109 |
transport=SSETransport(sse_server, headers={"X-DEMO-HEADER": "ABC"})
|
| 110 |
) as client:
|
| 111 |
raw_result = await client.read_resource("request://headers")
|
| 112 |
+
json_result = json.loads(raw_result[0].text) # type: ignore[attr-defined]
|
|
|
|
| 113 |
assert "x-demo-header" in json_result
|
| 114 |
assert json_result["x-demo-header"] == "ABC"
|
| 115 |
|
|
|
|
| 122 |
)
|
| 123 |
) as client:
|
| 124 |
result = await client.call_tool("get_headers_tool")
|
| 125 |
+
json_result = json.loads(result[0].text) # type: ignore[attr-defined]
|
|
|
|
| 126 |
assert "x-demo-header" in json_result
|
| 127 |
assert json_result["x-demo-header"] == "ABC"
|
| 128 |
|
|
|
|
| 132 |
transport=SSETransport(sse_server, headers={"X-DEMO-HEADER": "ABC"})
|
| 133 |
) as client:
|
| 134 |
result = await client.call_tool("get_headers_tool")
|
| 135 |
+
json_result = json.loads(result[0].text) # type: ignore[attr-defined]
|
|
|
|
| 136 |
assert "x-demo-header" in json_result
|
| 137 |
assert json_result["x-demo-header"] == "ABC"
|
| 138 |
|
|
|
|
| 145 |
)
|
| 146 |
) as client:
|
| 147 |
result = await client.get_prompt("get_headers_prompt")
|
| 148 |
+
json_result = json.loads(result.messages[0].content.text) # type: ignore[attr-defined]
|
|
|
|
| 149 |
assert "x-demo-header" in json_result
|
| 150 |
assert json_result["x-demo-header"] == "ABC"
|
| 151 |
|
|
|
|
| 156 |
transport=SSETransport(sse_server, headers={"X-DEMO-HEADER": "ABC"})
|
| 157 |
) as client:
|
| 158 |
result = await client.get_prompt("get_headers_prompt")
|
| 159 |
+
json_result = json.loads(result.messages[0].content.text) # type: ignore[attr-defined]
|
|
|
|
| 160 |
assert "x-demo-header" in json_result
|
| 161 |
assert json_result["x-demo-header"] == "ABC"
|
tests/server/openapi/test_openapi.py
CHANGED
|
@@ -9,7 +9,7 @@ from dirty_equals import IsStr
|
|
| 9 |
from fastapi import FastAPI, HTTPException, Response
|
| 10 |
from fastapi.responses import PlainTextResponse
|
| 11 |
from httpx import ASGITransport, AsyncClient
|
| 12 |
-
from mcp.types import BlobResourceContents
|
| 13 |
from pydantic import BaseModel, TypeAdapter
|
| 14 |
from pydantic.networks import AnyUrl
|
| 15 |
|
|
@@ -234,11 +234,7 @@ class TestTools:
|
|
| 234 |
"create_user_users_post", {"name": "David", "active": False}
|
| 235 |
)
|
| 236 |
|
| 237 |
-
|
| 238 |
-
assert isinstance(tool_response, list) and len(tool_response) == 1
|
| 239 |
-
assert isinstance(tool_response[0], TextContent)
|
| 240 |
-
|
| 241 |
-
response_data = json.loads(tool_response[0].text)
|
| 242 |
expected_user = User(id=4, name="David", active=False).model_dump()
|
| 243 |
assert response_data == expected_user
|
| 244 |
|
|
@@ -249,8 +245,7 @@ class TestTools:
|
|
| 249 |
# Check that the user was created via MCP
|
| 250 |
async with Client(fastmcp_openapi_server) as client:
|
| 251 |
user_response = await client.read_resource("resource://get_user_users/4")
|
| 252 |
-
|
| 253 |
-
response_text = user_response[0].text
|
| 254 |
user = json.loads(response_text)
|
| 255 |
assert user == expected_user
|
| 256 |
|
|
@@ -266,11 +261,7 @@ class TestTools:
|
|
| 266 |
{"user_id": 1, "name": "XYZ"},
|
| 267 |
)
|
| 268 |
|
| 269 |
-
|
| 270 |
-
assert isinstance(tool_response, list) and len(tool_response) == 1
|
| 271 |
-
assert isinstance(tool_response[0], TextContent)
|
| 272 |
-
|
| 273 |
-
response_data = json.loads(tool_response[0].text)
|
| 274 |
expected_data = dict(id=1, name="XYZ", active=True)
|
| 275 |
assert response_data == expected_data
|
| 276 |
|
|
@@ -281,8 +272,7 @@ class TestTools:
|
|
| 281 |
# Check that the user was updated via MCP
|
| 282 |
async with Client(fastmcp_openapi_server) as client:
|
| 283 |
user_response = await client.read_resource("resource://get_user_users/1")
|
| 284 |
-
|
| 285 |
-
response_text = user_response[0].text
|
| 286 |
user = json.loads(response_text)
|
| 287 |
assert user == expected_data
|
| 288 |
|
|
@@ -305,9 +295,7 @@ class TestTools:
|
|
| 305 |
)
|
| 306 |
async with Client(mcp_server) as client:
|
| 307 |
tool_response = await client.call_tool("get_users_users_get", {})
|
| 308 |
-
assert
|
| 309 |
-
assert isinstance(tool_response[0], TextContent)
|
| 310 |
-
assert json.loads(tool_response[0].text) == [
|
| 311 |
user.model_dump()
|
| 312 |
for user in sorted(users_db.values(), key=lambda x: x.id)
|
| 313 |
]
|
|
@@ -341,8 +329,7 @@ class TestResources:
|
|
| 341 |
resource_response = await client.read_resource(
|
| 342 |
"resource://get_users_users_get"
|
| 343 |
)
|
| 344 |
-
|
| 345 |
-
response_text = resource_response[0].text
|
| 346 |
resource = json.loads(response_text)
|
| 347 |
assert resource == json_users
|
| 348 |
response = await api_client.get("/users")
|
|
@@ -369,8 +356,7 @@ class TestResources:
|
|
| 369 |
"""Test reading a resource that returns a string."""
|
| 370 |
async with Client(fastmcp_openapi_server) as client:
|
| 371 |
resource_response = await client.read_resource("resource://ping_ping_get")
|
| 372 |
-
assert
|
| 373 |
-
assert resource_response[0].text == "pong"
|
| 374 |
|
| 375 |
|
| 376 |
class TestResourceTemplates:
|
|
@@ -407,8 +393,7 @@ class TestResourceTemplates:
|
|
| 407 |
resource_response = await client.read_resource(
|
| 408 |
f"resource://get_user_users/{user_id}"
|
| 409 |
)
|
| 410 |
-
|
| 411 |
-
response_text = resource_response[0].text
|
| 412 |
resource = json.loads(response_text)
|
| 413 |
|
| 414 |
assert resource == users_db[user_id].model_dump()
|
|
@@ -430,8 +415,7 @@ class TestResourceTemplates:
|
|
| 430 |
resource_response = await client.read_resource(
|
| 431 |
f"resource://get_user_active_state_users/{is_active}/{user_id}"
|
| 432 |
)
|
| 433 |
-
|
| 434 |
-
response_text = resource_response[0].text
|
| 435 |
resource = json.loads(response_text)
|
| 436 |
|
| 437 |
assert resource == users_db[user_id].model_dump()
|
|
@@ -681,8 +665,7 @@ class TestOpenAPI30Compatibility:
|
|
| 681 |
"""Test reading a resource from an OpenAPI 3.0 server."""
|
| 682 |
async with Client(openapi_30_server) as client:
|
| 683 |
resource_response = await client.read_resource("resource://listProducts")
|
| 684 |
-
|
| 685 |
-
response_text = resource_response[0].text
|
| 686 |
content = json.loads(response_text)
|
| 687 |
assert len(content) == 2
|
| 688 |
assert content[0]["name"] == "Product 1"
|
|
@@ -692,8 +675,7 @@ class TestOpenAPI30Compatibility:
|
|
| 692 |
"""Test reading a resource from template from an OpenAPI 3.0 server."""
|
| 693 |
async with Client(openapi_30_server) as client:
|
| 694 |
resource_response = await client.read_resource("resource://getProduct/p1")
|
| 695 |
-
|
| 696 |
-
response_text = resource_response[0].text
|
| 697 |
content = json.loads(response_text)
|
| 698 |
assert content["id"] == "p1"
|
| 699 |
assert content["name"] == "Product 1"
|
|
@@ -707,8 +689,7 @@ class TestOpenAPI30Compatibility:
|
|
| 707 |
)
|
| 708 |
# Result should be a text content
|
| 709 |
assert len(result) == 1
|
| 710 |
-
|
| 711 |
-
product = json.loads(result[0].text)
|
| 712 |
assert product["id"] == "p3"
|
| 713 |
assert product["name"] == "New Product"
|
| 714 |
assert product["price"] == 39.99
|
|
@@ -857,8 +838,7 @@ class TestOpenAPI31Compatibility:
|
|
| 857 |
"""Test reading a resource from an OpenAPI 3.1 server."""
|
| 858 |
async with Client(openapi_31_server) as client:
|
| 859 |
resource_response = await client.read_resource("resource://listOrders")
|
| 860 |
-
|
| 861 |
-
response_text = resource_response[0].text
|
| 862 |
content = json.loads(response_text)
|
| 863 |
assert len(content) == 2
|
| 864 |
assert content[0]["customer"] == "Alice"
|
|
@@ -868,8 +848,7 @@ class TestOpenAPI31Compatibility:
|
|
| 868 |
"""Test reading a resource from template from an OpenAPI 3.1 server."""
|
| 869 |
async with Client(openapi_31_server) as client:
|
| 870 |
resource_response = await client.read_resource("resource://getOrder/o1")
|
| 871 |
-
|
| 872 |
-
response_text = resource_response[0].text
|
| 873 |
content = json.loads(response_text)
|
| 874 |
assert content["id"] == "o1"
|
| 875 |
assert content["customer"] == "Alice"
|
|
@@ -883,8 +862,7 @@ class TestOpenAPI31Compatibility:
|
|
| 883 |
)
|
| 884 |
# Result should be a text content
|
| 885 |
assert len(result) == 1
|
| 886 |
-
|
| 887 |
-
order = json.loads(result[0].text)
|
| 888 |
assert order["id"] == "o3"
|
| 889 |
assert order["customer"] == "Charlie"
|
| 890 |
assert order["items"] == ["item4", "item5"]
|
|
|
|
| 9 |
from fastapi import FastAPI, HTTPException, Response
|
| 10 |
from fastapi.responses import PlainTextResponse
|
| 11 |
from httpx import ASGITransport, AsyncClient
|
| 12 |
+
from mcp.types import BlobResourceContents
|
| 13 |
from pydantic import BaseModel, TypeAdapter
|
| 14 |
from pydantic.networks import AnyUrl
|
| 15 |
|
|
|
|
| 234 |
"create_user_users_post", {"name": "David", "active": False}
|
| 235 |
)
|
| 236 |
|
| 237 |
+
response_data = json.loads(tool_response[0].text) # type: ignore[attr-defined]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
expected_user = User(id=4, name="David", active=False).model_dump()
|
| 239 |
assert response_data == expected_user
|
| 240 |
|
|
|
|
| 245 |
# Check that the user was created via MCP
|
| 246 |
async with Client(fastmcp_openapi_server) as client:
|
| 247 |
user_response = await client.read_resource("resource://get_user_users/4")
|
| 248 |
+
response_text = user_response[0].text # type: ignore[attr-defined]
|
|
|
|
| 249 |
user = json.loads(response_text)
|
| 250 |
assert user == expected_user
|
| 251 |
|
|
|
|
| 261 |
{"user_id": 1, "name": "XYZ"},
|
| 262 |
)
|
| 263 |
|
| 264 |
+
response_data = json.loads(tool_response[0].text) # type: ignore[attr-defined]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
expected_data = dict(id=1, name="XYZ", active=True)
|
| 266 |
assert response_data == expected_data
|
| 267 |
|
|
|
|
| 272 |
# Check that the user was updated via MCP
|
| 273 |
async with Client(fastmcp_openapi_server) as client:
|
| 274 |
user_response = await client.read_resource("resource://get_user_users/1")
|
| 275 |
+
response_text = user_response[0].text # type: ignore[attr-defined]
|
|
|
|
| 276 |
user = json.loads(response_text)
|
| 277 |
assert user == expected_data
|
| 278 |
|
|
|
|
| 295 |
)
|
| 296 |
async with Client(mcp_server) as client:
|
| 297 |
tool_response = await client.call_tool("get_users_users_get", {})
|
| 298 |
+
assert json.loads(tool_response[0].text) == [ # type: ignore[attr-defined]
|
|
|
|
|
|
|
| 299 |
user.model_dump()
|
| 300 |
for user in sorted(users_db.values(), key=lambda x: x.id)
|
| 301 |
]
|
|
|
|
| 329 |
resource_response = await client.read_resource(
|
| 330 |
"resource://get_users_users_get"
|
| 331 |
)
|
| 332 |
+
response_text = resource_response[0].text # type: ignore[attr-defined]
|
|
|
|
| 333 |
resource = json.loads(response_text)
|
| 334 |
assert resource == json_users
|
| 335 |
response = await api_client.get("/users")
|
|
|
|
| 356 |
"""Test reading a resource that returns a string."""
|
| 357 |
async with Client(fastmcp_openapi_server) as client:
|
| 358 |
resource_response = await client.read_resource("resource://ping_ping_get")
|
| 359 |
+
assert resource_response[0].text == "pong" # type: ignore[attr-defined]
|
|
|
|
| 360 |
|
| 361 |
|
| 362 |
class TestResourceTemplates:
|
|
|
|
| 393 |
resource_response = await client.read_resource(
|
| 394 |
f"resource://get_user_users/{user_id}"
|
| 395 |
)
|
| 396 |
+
response_text = resource_response[0].text # type: ignore[attr-defined]
|
|
|
|
| 397 |
resource = json.loads(response_text)
|
| 398 |
|
| 399 |
assert resource == users_db[user_id].model_dump()
|
|
|
|
| 415 |
resource_response = await client.read_resource(
|
| 416 |
f"resource://get_user_active_state_users/{is_active}/{user_id}"
|
| 417 |
)
|
| 418 |
+
response_text = resource_response[0].text # type: ignore[attr-defined]
|
|
|
|
| 419 |
resource = json.loads(response_text)
|
| 420 |
|
| 421 |
assert resource == users_db[user_id].model_dump()
|
|
|
|
| 665 |
"""Test reading a resource from an OpenAPI 3.0 server."""
|
| 666 |
async with Client(openapi_30_server) as client:
|
| 667 |
resource_response = await client.read_resource("resource://listProducts")
|
| 668 |
+
response_text = resource_response[0].text # type: ignore[attr-defined]
|
|
|
|
| 669 |
content = json.loads(response_text)
|
| 670 |
assert len(content) == 2
|
| 671 |
assert content[0]["name"] == "Product 1"
|
|
|
|
| 675 |
"""Test reading a resource from template from an OpenAPI 3.0 server."""
|
| 676 |
async with Client(openapi_30_server) as client:
|
| 677 |
resource_response = await client.read_resource("resource://getProduct/p1")
|
| 678 |
+
response_text = resource_response[0].text # type: ignore[attr-defined]
|
|
|
|
| 679 |
content = json.loads(response_text)
|
| 680 |
assert content["id"] == "p1"
|
| 681 |
assert content["name"] == "Product 1"
|
|
|
|
| 689 |
)
|
| 690 |
# Result should be a text content
|
| 691 |
assert len(result) == 1
|
| 692 |
+
product = json.loads(result[0].text) # type: ignore[attr-defined]
|
|
|
|
| 693 |
assert product["id"] == "p3"
|
| 694 |
assert product["name"] == "New Product"
|
| 695 |
assert product["price"] == 39.99
|
|
|
|
| 838 |
"""Test reading a resource from an OpenAPI 3.1 server."""
|
| 839 |
async with Client(openapi_31_server) as client:
|
| 840 |
resource_response = await client.read_resource("resource://listOrders")
|
| 841 |
+
response_text = resource_response[0].text # type: ignore[attr-defined]
|
|
|
|
| 842 |
content = json.loads(response_text)
|
| 843 |
assert len(content) == 2
|
| 844 |
assert content[0]["customer"] == "Alice"
|
|
|
|
| 848 |
"""Test reading a resource from template from an OpenAPI 3.1 server."""
|
| 849 |
async with Client(openapi_31_server) as client:
|
| 850 |
resource_response = await client.read_resource("resource://getOrder/o1")
|
| 851 |
+
response_text = resource_response[0].text # type: ignore[attr-defined]
|
|
|
|
| 852 |
content = json.loads(response_text)
|
| 853 |
assert content["id"] == "o1"
|
| 854 |
assert content["customer"] == "Alice"
|
|
|
|
| 862 |
)
|
| 863 |
# Result should be a text content
|
| 864 |
assert len(result) == 1
|
| 865 |
+
order = json.loads(result[0].text) # type: ignore[attr-dict]
|
|
|
|
| 866 |
assert order["id"] == "o3"
|
| 867 |
assert order["customer"] == "Charlie"
|
| 868 |
assert order["items"] == ["item4", "item5"]
|
tests/server/test_import_server.py
CHANGED
|
@@ -1,8 +1,6 @@
|
|
| 1 |
import json
|
| 2 |
from urllib.parse import quote
|
| 3 |
|
| 4 |
-
from mcp.types import TextContent, TextResourceContents
|
| 5 |
-
|
| 6 |
from fastmcp.client.client import Client
|
| 7 |
from fastmcp.server.server import FastMCP
|
| 8 |
|
|
@@ -223,8 +221,7 @@ async def test_call_imported_custom_named_tool():
|
|
| 223 |
|
| 224 |
async with Client(main_app) as client:
|
| 225 |
result = await client.call_tool("api_get_data", {"query": "test"})
|
| 226 |
-
assert
|
| 227 |
-
assert result[0].text == "Data for query: test"
|
| 228 |
|
| 229 |
|
| 230 |
async def test_first_level_importing_with_custom_name():
|
|
@@ -278,8 +275,7 @@ async def test_call_nested_imported_tool():
|
|
| 278 |
result = await main_app._tool_manager.call_tool(
|
| 279 |
"service_provider_compute", {"input": 21}
|
| 280 |
)
|
| 281 |
-
assert
|
| 282 |
-
assert result[0].text == "42"
|
| 283 |
|
| 284 |
|
| 285 |
async def test_import_with_proxy_tools():
|
|
@@ -302,8 +298,7 @@ async def test_import_with_proxy_tools():
|
|
| 302 |
await main_app.import_server("api", proxy_app)
|
| 303 |
|
| 304 |
result = await main_app._mcp_call_tool("api_get_data", {"query": "test"})
|
| 305 |
-
assert
|
| 306 |
-
assert result[0].text == "Data for query: test"
|
| 307 |
|
| 308 |
|
| 309 |
async def test_import_with_proxy_prompts():
|
|
@@ -326,7 +321,6 @@ async def test_import_with_proxy_prompts():
|
|
| 326 |
await main_app.import_server("api", proxy_app)
|
| 327 |
|
| 328 |
result = await main_app._mcp_get_prompt("api_greeting", {"name": "World"})
|
| 329 |
-
assert isinstance(result.messages[0].content, TextContent)
|
| 330 |
assert result.messages[0].content.text == "Hello, World from API!"
|
| 331 |
assert result.description == "Example greeting prompt."
|
| 332 |
|
|
@@ -356,8 +350,7 @@ async def test_import_with_proxy_resources():
|
|
| 356 |
# Access the resource through the main app with the prefixed key
|
| 357 |
async with Client(main_app) as client:
|
| 358 |
result = await client.read_resource("config://api/settings")
|
| 359 |
-
|
| 360 |
-
content = json.loads(result[0].text)
|
| 361 |
assert content["api_key"] == "12345"
|
| 362 |
assert content["base_url"] == "https://api.example.com"
|
| 363 |
|
|
@@ -387,8 +380,7 @@ async def test_import_with_proxy_resource_templates():
|
|
| 387 |
quoted_email = quote("john@example.com", safe="")
|
| 388 |
async with Client(main_app) as client:
|
| 389 |
result = await client.read_resource(f"user://api/{quoted_name}/{quoted_email}")
|
| 390 |
-
|
| 391 |
-
content = json.loads(result[0].text)
|
| 392 |
assert content["name"] == "John Doe"
|
| 393 |
assert content["email"] == "john@example.com"
|
| 394 |
|
|
|
|
| 1 |
import json
|
| 2 |
from urllib.parse import quote
|
| 3 |
|
|
|
|
|
|
|
| 4 |
from fastmcp.client.client import Client
|
| 5 |
from fastmcp.server.server import FastMCP
|
| 6 |
|
|
|
|
| 221 |
|
| 222 |
async with Client(main_app) as client:
|
| 223 |
result = await client.call_tool("api_get_data", {"query": "test"})
|
| 224 |
+
assert result[0].text == "Data for query: test" # type: ignore[attr-defined]
|
|
|
|
| 225 |
|
| 226 |
|
| 227 |
async def test_first_level_importing_with_custom_name():
|
|
|
|
| 275 |
result = await main_app._tool_manager.call_tool(
|
| 276 |
"service_provider_compute", {"input": 21}
|
| 277 |
)
|
| 278 |
+
assert result[0].text == "42" # type: ignore[attr-defined]
|
|
|
|
| 279 |
|
| 280 |
|
| 281 |
async def test_import_with_proxy_tools():
|
|
|
|
| 298 |
await main_app.import_server("api", proxy_app)
|
| 299 |
|
| 300 |
result = await main_app._mcp_call_tool("api_get_data", {"query": "test"})
|
| 301 |
+
assert result[0].text == "Data for query: test" # type: ignore[attr-defined]
|
|
|
|
| 302 |
|
| 303 |
|
| 304 |
async def test_import_with_proxy_prompts():
|
|
|
|
| 321 |
await main_app.import_server("api", proxy_app)
|
| 322 |
|
| 323 |
result = await main_app._mcp_get_prompt("api_greeting", {"name": "World"})
|
|
|
|
| 324 |
assert result.messages[0].content.text == "Hello, World from API!"
|
| 325 |
assert result.description == "Example greeting prompt."
|
| 326 |
|
|
|
|
| 350 |
# Access the resource through the main app with the prefixed key
|
| 351 |
async with Client(main_app) as client:
|
| 352 |
result = await client.read_resource("config://api/settings")
|
| 353 |
+
content = json.loads(result[0].text) # type: ignore[attr-defined]
|
|
|
|
| 354 |
assert content["api_key"] == "12345"
|
| 355 |
assert content["base_url"] == "https://api.example.com"
|
| 356 |
|
|
|
|
| 380 |
quoted_email = quote("john@example.com", safe="")
|
| 381 |
async with Client(main_app) as client:
|
| 382 |
result = await client.read_resource(f"user://api/{quoted_name}/{quoted_email}")
|
| 383 |
+
content = json.loads(result[0].text) # type: ignore[attr-defined]
|
|
|
|
| 384 |
assert content["name"] == "John Doe"
|
| 385 |
assert content["email"] == "john@example.com"
|
| 386 |
|
tests/server/test_mount.py
CHANGED
|
@@ -3,8 +3,6 @@ import sys
|
|
| 3 |
from contextlib import asynccontextmanager
|
| 4 |
|
| 5 |
import pytest
|
| 6 |
-
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
| 7 |
-
from mcp.types import TextContent, TextResourceContents
|
| 8 |
|
| 9 |
from fastmcp import FastMCP
|
| 10 |
from fastmcp.client import Client
|
|
@@ -36,8 +34,7 @@ class TestBasicMount:
|
|
| 36 |
|
| 37 |
async with Client(main_app) as client:
|
| 38 |
result = await client.call_tool("sub_sub_tool", {})
|
| 39 |
-
assert
|
| 40 |
-
assert result[0].text == "This is from the sub app"
|
| 41 |
|
| 42 |
async def test_mount_with_custom_separator(self):
|
| 43 |
"""Test mounting with a custom tool separator (deprecated but still supported)."""
|
|
@@ -57,8 +54,7 @@ class TestBasicMount:
|
|
| 57 |
|
| 58 |
# Call the tool
|
| 59 |
result = await main_app._mcp_call_tool("sub_greet", {"name": "World"})
|
| 60 |
-
assert
|
| 61 |
-
assert result[0].text == "Hello, World!"
|
| 62 |
|
| 63 |
async def test_mount_invalid_resource_prefix(self):
|
| 64 |
main_app = FastMCP("MainApp")
|
|
@@ -147,12 +143,10 @@ class TestMultipleServerMount:
|
|
| 147 |
|
| 148 |
# Call tools from both mounted servers
|
| 149 |
result1 = await main_app._mcp_call_tool("weather_get_forecast", {})
|
| 150 |
-
assert
|
| 151 |
-
assert result1[0].text == "Weather forecast"
|
| 152 |
|
| 153 |
result2 = await main_app._mcp_call_tool("news_get_headlines", {})
|
| 154 |
-
assert
|
| 155 |
-
assert result2[0].text == "News headlines"
|
| 156 |
|
| 157 |
async def test_mount_same_prefix(self):
|
| 158 |
"""Test that mounting with the same prefix replaces the previous mount."""
|
|
@@ -227,8 +221,7 @@ class TestMultipleServerMount:
|
|
| 227 |
|
| 228 |
# Test calling a tool
|
| 229 |
result = await client.call_tool("working_working_tool", {})
|
| 230 |
-
assert
|
| 231 |
-
assert result[0].text == "Working tool"
|
| 232 |
|
| 233 |
# Test resources
|
| 234 |
resources = await client.list_resources()
|
|
@@ -284,8 +277,7 @@ class TestDynamicChanges:
|
|
| 284 |
|
| 285 |
# Call the dynamically added tool
|
| 286 |
result = await main_app._mcp_call_tool("sub_dynamic_tool", {})
|
| 287 |
-
assert
|
| 288 |
-
assert result[0].text == "Added after mounting"
|
| 289 |
|
| 290 |
async def test_removing_tool_after_mounting(self):
|
| 291 |
"""Test that tools removed from mounted servers are no longer accessible."""
|
|
@@ -335,8 +327,7 @@ class TestResourcesAndTemplates:
|
|
| 335 |
# Check that resource can be accessed
|
| 336 |
async with Client(main_app) as client:
|
| 337 |
result = await client.read_resource("data://data/users")
|
| 338 |
-
assert
|
| 339 |
-
assert json.loads(result[0].text) == ["user1", "user2"]
|
| 340 |
|
| 341 |
async def test_mount_with_resource_templates(self):
|
| 342 |
"""Test mounting a server with resource templates."""
|
|
@@ -357,8 +348,7 @@ class TestResourcesAndTemplates:
|
|
| 357 |
# Check template instantiation
|
| 358 |
async with Client(main_app) as client:
|
| 359 |
result = await client.read_resource("users://api/123/profile")
|
| 360 |
-
|
| 361 |
-
profile = json.loads(result[0].text)
|
| 362 |
assert profile["id"] == "123"
|
| 363 |
assert profile["name"] == "User 123"
|
| 364 |
|
|
@@ -382,8 +372,7 @@ class TestResourcesAndTemplates:
|
|
| 382 |
# Check access to the resource
|
| 383 |
async with Client(main_app) as client:
|
| 384 |
result = await client.read_resource("data://data/config")
|
| 385 |
-
|
| 386 |
-
config = json.loads(result[0].text)
|
| 387 |
assert config["version"] == "1.0"
|
| 388 |
|
| 389 |
|
|
@@ -461,8 +450,7 @@ class TestProxyServer:
|
|
| 461 |
|
| 462 |
# Call the tool
|
| 463 |
result = await main_app._mcp_call_tool("proxy_get_data", {"query": "test"})
|
| 464 |
-
assert
|
| 465 |
-
assert result[0].text == "Data for test"
|
| 466 |
|
| 467 |
async def test_dynamically_adding_to_proxied_server(self):
|
| 468 |
"""Test that changes to the original server are reflected in the mounted proxy."""
|
|
@@ -489,8 +477,7 @@ class TestProxyServer:
|
|
| 489 |
|
| 490 |
# Call the tool
|
| 491 |
result = await main_app._mcp_call_tool("proxy_dynamic_data", {})
|
| 492 |
-
assert
|
| 493 |
-
assert result[0].text == "Dynamic data"
|
| 494 |
|
| 495 |
async def test_proxy_server_with_resources(self):
|
| 496 |
"""Test mounting a proxy server with resources."""
|
|
@@ -512,8 +499,7 @@ class TestProxyServer:
|
|
| 512 |
|
| 513 |
# Resource should be accessible through main app
|
| 514 |
result = await main_app._mcp_read_resource("config://proxy/settings")
|
| 515 |
-
|
| 516 |
-
config = json.loads(result[0].content)
|
| 517 |
assert config["api_key"] == "12345"
|
| 518 |
|
| 519 |
async def test_proxy_server_with_prompts(self):
|
|
|
|
| 3 |
from contextlib import asynccontextmanager
|
| 4 |
|
| 5 |
import pytest
|
|
|
|
|
|
|
| 6 |
|
| 7 |
from fastmcp import FastMCP
|
| 8 |
from fastmcp.client import Client
|
|
|
|
| 34 |
|
| 35 |
async with Client(main_app) as client:
|
| 36 |
result = await client.call_tool("sub_sub_tool", {})
|
| 37 |
+
assert result[0].text == "This is from the sub app" # type: ignore[attr-defined]
|
|
|
|
| 38 |
|
| 39 |
async def test_mount_with_custom_separator(self):
|
| 40 |
"""Test mounting with a custom tool separator (deprecated but still supported)."""
|
|
|
|
| 54 |
|
| 55 |
# Call the tool
|
| 56 |
result = await main_app._mcp_call_tool("sub_greet", {"name": "World"})
|
| 57 |
+
assert result[0].text == "Hello, World!" # type: ignore[attr-defined]
|
|
|
|
| 58 |
|
| 59 |
async def test_mount_invalid_resource_prefix(self):
|
| 60 |
main_app = FastMCP("MainApp")
|
|
|
|
| 143 |
|
| 144 |
# Call tools from both mounted servers
|
| 145 |
result1 = await main_app._mcp_call_tool("weather_get_forecast", {})
|
| 146 |
+
assert result1[0].text == "Weather forecast" # type: ignore[attr-defined]
|
|
|
|
| 147 |
|
| 148 |
result2 = await main_app._mcp_call_tool("news_get_headlines", {})
|
| 149 |
+
assert result2[0].text == "News headlines" # type: ignore[attr-defined]
|
|
|
|
| 150 |
|
| 151 |
async def test_mount_same_prefix(self):
|
| 152 |
"""Test that mounting with the same prefix replaces the previous mount."""
|
|
|
|
| 221 |
|
| 222 |
# Test calling a tool
|
| 223 |
result = await client.call_tool("working_working_tool", {})
|
| 224 |
+
assert result[0].text == "Working tool" # type: ignore[attr-defined]
|
|
|
|
| 225 |
|
| 226 |
# Test resources
|
| 227 |
resources = await client.list_resources()
|
|
|
|
| 277 |
|
| 278 |
# Call the dynamically added tool
|
| 279 |
result = await main_app._mcp_call_tool("sub_dynamic_tool", {})
|
| 280 |
+
assert result[0].text == "Added after mounting" # type: ignore[attr-defined]
|
|
|
|
| 281 |
|
| 282 |
async def test_removing_tool_after_mounting(self):
|
| 283 |
"""Test that tools removed from mounted servers are no longer accessible."""
|
|
|
|
| 327 |
# Check that resource can be accessed
|
| 328 |
async with Client(main_app) as client:
|
| 329 |
result = await client.read_resource("data://data/users")
|
| 330 |
+
assert json.loads(result[0].text) == ["user1", "user2"] # type: ignore[attr-defined]
|
|
|
|
| 331 |
|
| 332 |
async def test_mount_with_resource_templates(self):
|
| 333 |
"""Test mounting a server with resource templates."""
|
|
|
|
| 348 |
# Check template instantiation
|
| 349 |
async with Client(main_app) as client:
|
| 350 |
result = await client.read_resource("users://api/123/profile")
|
| 351 |
+
profile = json.loads(result[0].text) # type: ignore
|
|
|
|
| 352 |
assert profile["id"] == "123"
|
| 353 |
assert profile["name"] == "User 123"
|
| 354 |
|
|
|
|
| 372 |
# Check access to the resource
|
| 373 |
async with Client(main_app) as client:
|
| 374 |
result = await client.read_resource("data://data/config")
|
| 375 |
+
config = json.loads(result[0].text) # type: ignore[attr-defined]
|
|
|
|
| 376 |
assert config["version"] == "1.0"
|
| 377 |
|
| 378 |
|
|
|
|
| 450 |
|
| 451 |
# Call the tool
|
| 452 |
result = await main_app._mcp_call_tool("proxy_get_data", {"query": "test"})
|
| 453 |
+
assert result[0].text == "Data for test" # type: ignore[attr-defined]
|
|
|
|
| 454 |
|
| 455 |
async def test_dynamically_adding_to_proxied_server(self):
|
| 456 |
"""Test that changes to the original server are reflected in the mounted proxy."""
|
|
|
|
| 477 |
|
| 478 |
# Call the tool
|
| 479 |
result = await main_app._mcp_call_tool("proxy_dynamic_data", {})
|
| 480 |
+
assert result[0].text == "Dynamic data" # type: ignore[attr-defined]
|
|
|
|
| 481 |
|
| 482 |
async def test_proxy_server_with_resources(self):
|
| 483 |
"""Test mounting a proxy server with resources."""
|
|
|
|
| 499 |
|
| 500 |
# Resource should be accessible through main app
|
| 501 |
result = await main_app._mcp_read_resource("config://proxy/settings")
|
| 502 |
+
config = json.loads(result[0].content) # type: ignore[attr-defined]
|
|
|
|
| 503 |
assert config["api_key"] == "12345"
|
| 504 |
|
| 505 |
async def test_proxy_server_with_prompts(self):
|
tests/server/test_proxy.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
import json
|
| 2 |
from typing import Any
|
| 3 |
|
| 4 |
-
import mcp.types
|
| 5 |
import pytest
|
| 6 |
from anyio import create_task_group
|
| 7 |
from dirty_equals import Contains
|
|
@@ -90,16 +89,14 @@ async def test_as_proxy_with_server(fastmcp_server):
|
|
| 90 |
"""FastMCP.as_proxy should accept a FastMCP instance."""
|
| 91 |
proxy = FastMCP.as_proxy(fastmcp_server)
|
| 92 |
result = await proxy._mcp_call_tool("greet", {"name": "Test"})
|
| 93 |
-
assert
|
| 94 |
-
assert result[0].text == "Hello, Test!"
|
| 95 |
|
| 96 |
|
| 97 |
async def test_as_proxy_with_transport(fastmcp_server):
|
| 98 |
"""FastMCP.as_proxy should accept a ClientTransport."""
|
| 99 |
proxy = FastMCP.as_proxy(FastMCPTransport(fastmcp_server))
|
| 100 |
result = await proxy._mcp_call_tool("greet", {"name": "Test"})
|
| 101 |
-
assert
|
| 102 |
-
assert result[0].text == "Hello, Test!"
|
| 103 |
|
| 104 |
|
| 105 |
def test_as_proxy_with_url():
|
|
@@ -138,9 +135,7 @@ class TestTools:
|
|
| 138 |
async def test_call_tool_calls_tool(self, proxy_server):
|
| 139 |
async with Client(proxy_server) as client:
|
| 140 |
proxy_result = await client.call_tool("add", {"a": 1, "b": 2})
|
| 141 |
-
|
| 142 |
-
assert isinstance(proxy_result[0], mcp.types.TextContent)
|
| 143 |
-
assert proxy_result[0].text == "3"
|
| 144 |
|
| 145 |
async def test_error_tool_raises_error(self, proxy_server):
|
| 146 |
with pytest.raises(ToolError, match=""):
|
|
@@ -164,8 +159,7 @@ class TestResources:
|
|
| 164 |
async def test_read_resource(self, proxy_server: FastMCPProxy):
|
| 165 |
async with Client(proxy_server) as client:
|
| 166 |
result = await client.read_resource("resource://wave")
|
| 167 |
-
assert
|
| 168 |
-
assert result[0].text == "👋"
|
| 169 |
|
| 170 |
async def test_read_resource_same_as_original(self, fastmcp_server, proxy_server):
|
| 171 |
async with Client(fastmcp_server) as client:
|
|
@@ -177,8 +171,7 @@ class TestResources:
|
|
| 177 |
async def test_read_json_resource(self, proxy_server: FastMCPProxy):
|
| 178 |
async with Client(proxy_server) as client:
|
| 179 |
result = await client.read_resource("data://users")
|
| 180 |
-
assert
|
| 181 |
-
assert json.loads(result[0].text) == USERS
|
| 182 |
|
| 183 |
async def test_read_resource_returns_none_if_not_found(self, proxy_server):
|
| 184 |
with pytest.raises(McpError, match="Unknown resource: resource://nonexistent"):
|
|
@@ -202,8 +195,7 @@ class TestResourceTemplates:
|
|
| 202 |
async def test_read_resource_template(self, proxy_server: FastMCPProxy, id: int):
|
| 203 |
async with Client(proxy_server) as client:
|
| 204 |
result = await client.read_resource(f"data://user/{id}")
|
| 205 |
-
assert
|
| 206 |
-
assert json.loads(result[0].text) == USERS[id - 1]
|
| 207 |
|
| 208 |
async def test_read_resource_template_same_as_original(
|
| 209 |
self, fastmcp_server, proxy_server
|
|
@@ -239,10 +231,8 @@ class TestPrompts:
|
|
| 239 |
async def test_render_prompt_calls_prompt(self, proxy_server):
|
| 240 |
async with Client(proxy_server) as client:
|
| 241 |
result = await client.get_prompt("welcome", {"name": "Alice"})
|
| 242 |
-
assert isinstance(result.messages[0], mcp.types.PromptMessage)
|
| 243 |
assert result.messages[0].role == "user"
|
| 244 |
-
assert
|
| 245 |
-
assert result.messages[0].content.text == "Welcome to FastMCP, Alice!"
|
| 246 |
|
| 247 |
|
| 248 |
async def test_proxy_handles_multiple_concurrent_tasks_correctly(
|
|
|
|
| 1 |
import json
|
| 2 |
from typing import Any
|
| 3 |
|
|
|
|
| 4 |
import pytest
|
| 5 |
from anyio import create_task_group
|
| 6 |
from dirty_equals import Contains
|
|
|
|
| 89 |
"""FastMCP.as_proxy should accept a FastMCP instance."""
|
| 90 |
proxy = FastMCP.as_proxy(fastmcp_server)
|
| 91 |
result = await proxy._mcp_call_tool("greet", {"name": "Test"})
|
| 92 |
+
assert result[0].text == "Hello, Test!" # type: ignore[attr-defined]
|
|
|
|
| 93 |
|
| 94 |
|
| 95 |
async def test_as_proxy_with_transport(fastmcp_server):
|
| 96 |
"""FastMCP.as_proxy should accept a ClientTransport."""
|
| 97 |
proxy = FastMCP.as_proxy(FastMCPTransport(fastmcp_server))
|
| 98 |
result = await proxy._mcp_call_tool("greet", {"name": "Test"})
|
| 99 |
+
assert result[0].text == "Hello, Test!" # type: ignore[attr-defined]
|
|
|
|
| 100 |
|
| 101 |
|
| 102 |
def test_as_proxy_with_url():
|
|
|
|
| 135 |
async def test_call_tool_calls_tool(self, proxy_server):
|
| 136 |
async with Client(proxy_server) as client:
|
| 137 |
proxy_result = await client.call_tool("add", {"a": 1, "b": 2})
|
| 138 |
+
assert proxy_result[0].text == "3" # type: ignore[attr-defined]
|
|
|
|
|
|
|
| 139 |
|
| 140 |
async def test_error_tool_raises_error(self, proxy_server):
|
| 141 |
with pytest.raises(ToolError, match=""):
|
|
|
|
| 159 |
async def test_read_resource(self, proxy_server: FastMCPProxy):
|
| 160 |
async with Client(proxy_server) as client:
|
| 161 |
result = await client.read_resource("resource://wave")
|
| 162 |
+
assert result[0].text == "👋" # type: ignore[attr-defined]
|
|
|
|
| 163 |
|
| 164 |
async def test_read_resource_same_as_original(self, fastmcp_server, proxy_server):
|
| 165 |
async with Client(fastmcp_server) as client:
|
|
|
|
| 171 |
async def test_read_json_resource(self, proxy_server: FastMCPProxy):
|
| 172 |
async with Client(proxy_server) as client:
|
| 173 |
result = await client.read_resource("data://users")
|
| 174 |
+
assert json.loads(result[0].text) == USERS # type: ignore[attr-defined]
|
|
|
|
| 175 |
|
| 176 |
async def test_read_resource_returns_none_if_not_found(self, proxy_server):
|
| 177 |
with pytest.raises(McpError, match="Unknown resource: resource://nonexistent"):
|
|
|
|
| 195 |
async def test_read_resource_template(self, proxy_server: FastMCPProxy, id: int):
|
| 196 |
async with Client(proxy_server) as client:
|
| 197 |
result = await client.read_resource(f"data://user/{id}")
|
| 198 |
+
assert json.loads(result[0].text) == USERS[id - 1] # type: ignore[attr-defined]
|
|
|
|
| 199 |
|
| 200 |
async def test_read_resource_template_same_as_original(
|
| 201 |
self, fastmcp_server, proxy_server
|
|
|
|
| 231 |
async def test_render_prompt_calls_prompt(self, proxy_server):
|
| 232 |
async with Client(proxy_server) as client:
|
| 233 |
result = await client.get_prompt("welcome", {"name": "Alice"})
|
|
|
|
| 234 |
assert result.messages[0].role == "user"
|
| 235 |
+
assert result.messages[0].content.text == "Welcome to FastMCP, Alice!" # type: ignore[attr-defined]
|
|
|
|
| 236 |
|
| 237 |
|
| 238 |
async def test_proxy_handles_multiple_concurrent_tasks_correctly(
|
tests/server/test_server.py
CHANGED
|
@@ -2,10 +2,6 @@ from typing import Annotated
|
|
| 2 |
|
| 3 |
import pytest
|
| 4 |
from mcp import McpError
|
| 5 |
-
from mcp.types import (
|
| 6 |
-
TextContent,
|
| 7 |
-
TextResourceContents,
|
| 8 |
-
)
|
| 9 |
from pydantic import Field
|
| 10 |
|
| 11 |
from fastmcp import Client, FastMCP
|
|
@@ -48,8 +44,7 @@ class TestCreateServer:
|
|
| 48 |
result = await client.call_tool("hello_world", {})
|
| 49 |
assert len(result) == 1
|
| 50 |
content = result[0]
|
| 51 |
-
assert
|
| 52 |
-
assert "¡Hola, 世界! 👋" == content.text
|
| 53 |
|
| 54 |
|
| 55 |
class TestTools:
|
|
@@ -114,8 +109,7 @@ class TestToolDecorator:
|
|
| 114 |
return x + y
|
| 115 |
|
| 116 |
result = await mcp._mcp_call_tool("add", {"x": 1, "y": 2})
|
| 117 |
-
assert
|
| 118 |
-
assert result[0].text == "3"
|
| 119 |
|
| 120 |
async def test_tool_decorator_incorrect_usage(self):
|
| 121 |
mcp = FastMCP()
|
|
@@ -134,8 +128,7 @@ class TestToolDecorator:
|
|
| 134 |
return x + y
|
| 135 |
|
| 136 |
result = await mcp._mcp_call_tool("custom-add", {"x": 1, "y": 2})
|
| 137 |
-
assert
|
| 138 |
-
assert result[0].text == "3"
|
| 139 |
|
| 140 |
async def test_tool_decorator_with_description(self):
|
| 141 |
mcp = FastMCP()
|
|
@@ -163,8 +156,7 @@ class TestToolDecorator:
|
|
| 163 |
obj = MyClass(10)
|
| 164 |
mcp.add_tool(obj.add)
|
| 165 |
result = await mcp._mcp_call_tool("add", {"y": 2})
|
| 166 |
-
assert
|
| 167 |
-
assert result[0].text == "12"
|
| 168 |
|
| 169 |
async def test_tool_decorator_classmethod(self):
|
| 170 |
mcp = FastMCP()
|
|
@@ -178,8 +170,7 @@ class TestToolDecorator:
|
|
| 178 |
|
| 179 |
mcp.add_tool(MyClass.add)
|
| 180 |
result = await mcp._mcp_call_tool("add", {"y": 2})
|
| 181 |
-
assert
|
| 182 |
-
assert result[0].text == "12"
|
| 183 |
|
| 184 |
async def test_tool_decorator_staticmethod(self):
|
| 185 |
mcp = FastMCP()
|
|
@@ -191,8 +182,7 @@ class TestToolDecorator:
|
|
| 191 |
return x + y
|
| 192 |
|
| 193 |
result = await mcp._mcp_call_tool("add", {"x": 1, "y": 2})
|
| 194 |
-
assert
|
| 195 |
-
assert result[0].text == "3"
|
| 196 |
|
| 197 |
async def test_tool_decorator_async_function(self):
|
| 198 |
mcp = FastMCP()
|
|
@@ -202,8 +192,7 @@ class TestToolDecorator:
|
|
| 202 |
return x + y
|
| 203 |
|
| 204 |
result = await mcp._mcp_call_tool("add", {"x": 1, "y": 2})
|
| 205 |
-
assert
|
| 206 |
-
assert result[0].text == "3"
|
| 207 |
|
| 208 |
async def test_tool_decorator_classmethod_async_function(self):
|
| 209 |
mcp = FastMCP()
|
|
@@ -217,8 +206,7 @@ class TestToolDecorator:
|
|
| 217 |
|
| 218 |
mcp.add_tool(MyClass.add)
|
| 219 |
result = await mcp._mcp_call_tool("add", {"y": 2})
|
| 220 |
-
assert
|
| 221 |
-
assert result[0].text == "12"
|
| 222 |
|
| 223 |
async def test_tool_decorator_staticmethod_async_function(self):
|
| 224 |
mcp = FastMCP()
|
|
@@ -230,8 +218,7 @@ class TestToolDecorator:
|
|
| 230 |
|
| 231 |
mcp.add_tool(MyClass.add)
|
| 232 |
result = await mcp._mcp_call_tool("add", {"x": 1, "y": 2})
|
| 233 |
-
assert
|
| 234 |
-
assert result[0].text == "3"
|
| 235 |
|
| 236 |
async def test_tool_decorator_with_tags(self):
|
| 237 |
"""Test that the tool decorator properly sets tags."""
|
|
@@ -262,8 +249,7 @@ class TestToolDecorator:
|
|
| 262 |
|
| 263 |
# Call the tool by its custom name
|
| 264 |
result = await mcp._mcp_call_tool("custom_multiply", {"a": 5, "b": 3})
|
| 265 |
-
assert
|
| 266 |
-
assert result[0].text == "15"
|
| 267 |
|
| 268 |
# Original name should not be registered
|
| 269 |
assert "multiply" not in tools
|
|
@@ -316,8 +302,7 @@ class TestResourceDecorator:
|
|
| 316 |
|
| 317 |
async with Client(mcp) as client:
|
| 318 |
result = await client.read_resource("resource://data")
|
| 319 |
-
assert
|
| 320 |
-
assert result[0].text == "Hello, world!"
|
| 321 |
|
| 322 |
async def test_resource_decorator_incorrect_usage(self):
|
| 323 |
mcp = FastMCP()
|
|
@@ -344,8 +329,7 @@ class TestResourceDecorator:
|
|
| 344 |
|
| 345 |
async with Client(mcp) as client:
|
| 346 |
result = await client.read_resource("resource://data")
|
| 347 |
-
assert
|
| 348 |
-
assert result[0].text == "Hello, world!"
|
| 349 |
|
| 350 |
async def test_resource_decorator_with_description(self):
|
| 351 |
mcp = FastMCP()
|
|
@@ -389,8 +373,7 @@ class TestResourceDecorator:
|
|
| 389 |
|
| 390 |
async with Client(mcp) as client:
|
| 391 |
result = await client.read_resource("resource://data")
|
| 392 |
-
assert
|
| 393 |
-
assert result[0].text == "My prefix: Hello, world!"
|
| 394 |
|
| 395 |
async def test_resource_decorator_classmethod(self):
|
| 396 |
mcp = FastMCP()
|
|
@@ -408,8 +391,7 @@ class TestResourceDecorator:
|
|
| 408 |
|
| 409 |
async with Client(mcp) as client:
|
| 410 |
result = await client.read_resource("resource://data")
|
| 411 |
-
assert
|
| 412 |
-
assert result[0].text == "Class prefix: Hello, world!"
|
| 413 |
|
| 414 |
async def test_resource_decorator_staticmethod(self):
|
| 415 |
mcp = FastMCP()
|
|
@@ -422,8 +404,7 @@ class TestResourceDecorator:
|
|
| 422 |
|
| 423 |
async with Client(mcp) as client:
|
| 424 |
result = await client.read_resource("resource://data")
|
| 425 |
-
assert
|
| 426 |
-
assert result[0].text == "Static Hello, world!"
|
| 427 |
|
| 428 |
async def test_resource_decorator_async_function(self):
|
| 429 |
mcp = FastMCP()
|
|
@@ -434,8 +415,7 @@ class TestResourceDecorator:
|
|
| 434 |
|
| 435 |
async with Client(mcp) as client:
|
| 436 |
result = await client.read_resource("resource://data")
|
| 437 |
-
assert
|
| 438 |
-
assert result[0].text == "Async Hello, world!"
|
| 439 |
|
| 440 |
|
| 441 |
class TestTemplateDecorator:
|
|
@@ -454,8 +434,7 @@ class TestTemplateDecorator:
|
|
| 454 |
|
| 455 |
async with Client(mcp) as client:
|
| 456 |
result = await client.read_resource("resource://test/data")
|
| 457 |
-
assert
|
| 458 |
-
assert result[0].text == "Data for test"
|
| 459 |
|
| 460 |
async def test_template_decorator_incorrect_usage(self):
|
| 461 |
mcp = FastMCP()
|
|
@@ -482,8 +461,7 @@ class TestTemplateDecorator:
|
|
| 482 |
|
| 483 |
async with Client(mcp) as client:
|
| 484 |
result = await client.read_resource("resource://test/data")
|
| 485 |
-
assert
|
| 486 |
-
assert result[0].text == "Data for test"
|
| 487 |
|
| 488 |
async def test_template_decorator_with_description(self):
|
| 489 |
mcp = FastMCP()
|
|
@@ -514,8 +492,7 @@ class TestTemplateDecorator:
|
|
| 514 |
|
| 515 |
async with Client(mcp) as client:
|
| 516 |
result = await client.read_resource("resource://test/data")
|
| 517 |
-
assert
|
| 518 |
-
assert result[0].text == "My prefix: Data for test"
|
| 519 |
|
| 520 |
async def test_template_decorator_classmethod(self):
|
| 521 |
mcp = FastMCP()
|
|
@@ -535,8 +512,7 @@ class TestTemplateDecorator:
|
|
| 535 |
|
| 536 |
async with Client(mcp) as client:
|
| 537 |
result = await client.read_resource("resource://test/data")
|
| 538 |
-
assert
|
| 539 |
-
assert result[0].text == "Class prefix: Data for test"
|
| 540 |
|
| 541 |
async def test_template_decorator_staticmethod(self):
|
| 542 |
mcp = FastMCP()
|
|
@@ -549,8 +525,7 @@ class TestTemplateDecorator:
|
|
| 549 |
|
| 550 |
async with Client(mcp) as client:
|
| 551 |
result = await client.read_resource("resource://test/data")
|
| 552 |
-
assert
|
| 553 |
-
assert result[0].text == "Static Data for test"
|
| 554 |
|
| 555 |
async def test_template_decorator_async_function(self):
|
| 556 |
mcp = FastMCP()
|
|
@@ -561,8 +536,7 @@ class TestTemplateDecorator:
|
|
| 561 |
|
| 562 |
async with Client(mcp) as client:
|
| 563 |
result = await client.read_resource("resource://test/data")
|
| 564 |
-
assert
|
| 565 |
-
assert result[0].text == "Async Data for test"
|
| 566 |
|
| 567 |
async def test_template_decorator_with_tags(self):
|
| 568 |
"""Test that the template decorator properly sets tags."""
|
|
@@ -603,8 +577,7 @@ class TestPromptDecorator:
|
|
| 603 |
assert prompt.name == "fn"
|
| 604 |
# Don't compare functions directly since validate_call wraps them
|
| 605 |
content = await prompt.render()
|
| 606 |
-
assert
|
| 607 |
-
assert content[0].content.text == "Hello, world!"
|
| 608 |
|
| 609 |
async def test_prompt_decorator_incorrect_usage(self):
|
| 610 |
mcp = FastMCP()
|
|
@@ -629,8 +602,7 @@ class TestPromptDecorator:
|
|
| 629 |
prompt = prompts_dict["custom_name"]
|
| 630 |
assert prompt.name == "custom_name"
|
| 631 |
content = await prompt.render()
|
| 632 |
-
assert
|
| 633 |
-
assert content[0].content.text == "Hello, world!"
|
| 634 |
|
| 635 |
async def test_prompt_decorator_with_description(self):
|
| 636 |
mcp = FastMCP()
|
|
@@ -644,8 +616,7 @@ class TestPromptDecorator:
|
|
| 644 |
prompt = prompts_dict["fn"]
|
| 645 |
assert prompt.description == "A custom description"
|
| 646 |
content = await prompt.render()
|
| 647 |
-
assert
|
| 648 |
-
assert content[0].content.text == "Hello, world!"
|
| 649 |
|
| 650 |
async def test_prompt_decorator_with_parameters(self):
|
| 651 |
mcp = FastMCP()
|
|
@@ -668,16 +639,14 @@ class TestPromptDecorator:
|
|
| 668 |
result = await client.get_prompt("test_prompt", {"name": "World"})
|
| 669 |
assert len(result.messages) == 1
|
| 670 |
message = result.messages[0]
|
| 671 |
-
assert
|
| 672 |
-
assert message.content.text == "Hello, World!"
|
| 673 |
|
| 674 |
result = await client.get_prompt(
|
| 675 |
"test_prompt", {"name": "World", "greeting": "Hi"}
|
| 676 |
)
|
| 677 |
assert len(result.messages) == 1
|
| 678 |
message = result.messages[0]
|
| 679 |
-
assert
|
| 680 |
-
assert message.content.text == "Hi, World!"
|
| 681 |
|
| 682 |
async def test_prompt_decorator_instance_method(self):
|
| 683 |
mcp = FastMCP()
|
|
@@ -696,8 +665,7 @@ class TestPromptDecorator:
|
|
| 696 |
result = await client.get_prompt("test_prompt")
|
| 697 |
assert len(result.messages) == 1
|
| 698 |
message = result.messages[0]
|
| 699 |
-
assert
|
| 700 |
-
assert message.content.text == "My prefix: Hello, world!"
|
| 701 |
|
| 702 |
async def test_prompt_decorator_classmethod(self):
|
| 703 |
mcp = FastMCP()
|
|
@@ -715,8 +683,7 @@ class TestPromptDecorator:
|
|
| 715 |
result = await client.get_prompt("test_prompt")
|
| 716 |
assert len(result.messages) == 1
|
| 717 |
message = result.messages[0]
|
| 718 |
-
assert
|
| 719 |
-
assert message.content.text == "Class prefix: Hello, world!"
|
| 720 |
|
| 721 |
async def test_prompt_decorator_staticmethod(self):
|
| 722 |
mcp = FastMCP()
|
|
@@ -731,8 +698,7 @@ class TestPromptDecorator:
|
|
| 731 |
result = await client.get_prompt("test_prompt")
|
| 732 |
assert len(result.messages) == 1
|
| 733 |
message = result.messages[0]
|
| 734 |
-
assert
|
| 735 |
-
assert message.content.text == "Static Hello, world!"
|
| 736 |
|
| 737 |
async def test_prompt_decorator_async_function(self):
|
| 738 |
mcp = FastMCP()
|
|
@@ -745,8 +711,7 @@ class TestPromptDecorator:
|
|
| 745 |
result = await client.get_prompt("test_prompt")
|
| 746 |
assert len(result.messages) == 1
|
| 747 |
message = result.messages[0]
|
| 748 |
-
assert
|
| 749 |
-
assert message.content.text == "Async Hello, world!"
|
| 750 |
|
| 751 |
async def test_prompt_decorator_with_tags(self):
|
| 752 |
"""Test that the prompt decorator properly sets tags."""
|
|
@@ -943,20 +908,17 @@ class TestResourcePrefixMounting:
|
|
| 943 |
async with Client(main_server) as client:
|
| 944 |
# Regular resource
|
| 945 |
result = await client.read_resource("resource://prefix/test-resource")
|
| 946 |
-
assert
|
| 947 |
-
assert result[0].text == "Resource content"
|
| 948 |
|
| 949 |
# Absolute path resource
|
| 950 |
result = await client.read_resource("resource://prefix//absolute/path")
|
| 951 |
-
assert
|
| 952 |
-
assert result[0].text == "Absolute resource content"
|
| 953 |
|
| 954 |
# Template resource
|
| 955 |
result = await client.read_resource(
|
| 956 |
"resource://prefix/param-value/template"
|
| 957 |
)
|
| 958 |
-
assert
|
| 959 |
-
assert result[0].text == "Template resource with param-value"
|
| 960 |
|
| 961 |
@pytest.mark.parametrize(
|
| 962 |
"uri,prefix,expected_match,expected_strip",
|
|
@@ -1032,15 +994,12 @@ class TestResourcePrefixMounting:
|
|
| 1032 |
# Verify we can access the resources
|
| 1033 |
async with Client(target_server) as client:
|
| 1034 |
result = await client.read_resource("resource://imported/test-resource")
|
| 1035 |
-
assert
|
| 1036 |
-
assert result[0].text == "Resource content"
|
| 1037 |
|
| 1038 |
result = await client.read_resource("resource://imported//absolute/path")
|
| 1039 |
-
assert
|
| 1040 |
-
assert result[0].text == "Absolute resource content"
|
| 1041 |
|
| 1042 |
result = await client.read_resource(
|
| 1043 |
"resource://imported/param-value/template"
|
| 1044 |
)
|
| 1045 |
-
assert
|
| 1046 |
-
assert result[0].text == "Template resource with param-value"
|
|
|
|
| 2 |
|
| 3 |
import pytest
|
| 4 |
from mcp import McpError
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
from pydantic import Field
|
| 6 |
|
| 7 |
from fastmcp import Client, FastMCP
|
|
|
|
| 44 |
result = await client.call_tool("hello_world", {})
|
| 45 |
assert len(result) == 1
|
| 46 |
content = result[0]
|
| 47 |
+
assert content.text == "¡Hola, 世界! 👋" # type: ignore[attr-defined]
|
|
|
|
| 48 |
|
| 49 |
|
| 50 |
class TestTools:
|
|
|
|
| 109 |
return x + y
|
| 110 |
|
| 111 |
result = await mcp._mcp_call_tool("add", {"x": 1, "y": 2})
|
| 112 |
+
assert result[0].text == "3" # type: ignore[attr-defined]
|
|
|
|
| 113 |
|
| 114 |
async def test_tool_decorator_incorrect_usage(self):
|
| 115 |
mcp = FastMCP()
|
|
|
|
| 128 |
return x + y
|
| 129 |
|
| 130 |
result = await mcp._mcp_call_tool("custom-add", {"x": 1, "y": 2})
|
| 131 |
+
assert result[0].text == "3" # type: ignore[attr-defined]
|
|
|
|
| 132 |
|
| 133 |
async def test_tool_decorator_with_description(self):
|
| 134 |
mcp = FastMCP()
|
|
|
|
| 156 |
obj = MyClass(10)
|
| 157 |
mcp.add_tool(obj.add)
|
| 158 |
result = await mcp._mcp_call_tool("add", {"y": 2})
|
| 159 |
+
assert result[0].text == "12" # type: ignore[attr-defined]
|
|
|
|
| 160 |
|
| 161 |
async def test_tool_decorator_classmethod(self):
|
| 162 |
mcp = FastMCP()
|
|
|
|
| 170 |
|
| 171 |
mcp.add_tool(MyClass.add)
|
| 172 |
result = await mcp._mcp_call_tool("add", {"y": 2})
|
| 173 |
+
assert result[0].text == "12" # type: ignore[attr-defined]
|
|
|
|
| 174 |
|
| 175 |
async def test_tool_decorator_staticmethod(self):
|
| 176 |
mcp = FastMCP()
|
|
|
|
| 182 |
return x + y
|
| 183 |
|
| 184 |
result = await mcp._mcp_call_tool("add", {"x": 1, "y": 2})
|
| 185 |
+
assert result[0].text == "3" # type: ignore[attr-defined]
|
|
|
|
| 186 |
|
| 187 |
async def test_tool_decorator_async_function(self):
|
| 188 |
mcp = FastMCP()
|
|
|
|
| 192 |
return x + y
|
| 193 |
|
| 194 |
result = await mcp._mcp_call_tool("add", {"x": 1, "y": 2})
|
| 195 |
+
assert result[0].text == "3" # type: ignore[attr-defined]
|
|
|
|
| 196 |
|
| 197 |
async def test_tool_decorator_classmethod_async_function(self):
|
| 198 |
mcp = FastMCP()
|
|
|
|
| 206 |
|
| 207 |
mcp.add_tool(MyClass.add)
|
| 208 |
result = await mcp._mcp_call_tool("add", {"y": 2})
|
| 209 |
+
assert result[0].text == "12" # type: ignore[attr-defined]
|
|
|
|
| 210 |
|
| 211 |
async def test_tool_decorator_staticmethod_async_function(self):
|
| 212 |
mcp = FastMCP()
|
|
|
|
| 218 |
|
| 219 |
mcp.add_tool(MyClass.add)
|
| 220 |
result = await mcp._mcp_call_tool("add", {"x": 1, "y": 2})
|
| 221 |
+
assert result[0].text == "3" # type: ignore[attr-defined]
|
|
|
|
| 222 |
|
| 223 |
async def test_tool_decorator_with_tags(self):
|
| 224 |
"""Test that the tool decorator properly sets tags."""
|
|
|
|
| 249 |
|
| 250 |
# Call the tool by its custom name
|
| 251 |
result = await mcp._mcp_call_tool("custom_multiply", {"a": 5, "b": 3})
|
| 252 |
+
assert result[0].text == "15" # type: ignore[attr-defined]
|
|
|
|
| 253 |
|
| 254 |
# Original name should not be registered
|
| 255 |
assert "multiply" not in tools
|
|
|
|
| 302 |
|
| 303 |
async with Client(mcp) as client:
|
| 304 |
result = await client.read_resource("resource://data")
|
| 305 |
+
assert result[0].text == "Hello, world!" # type: ignore[attr-defined]
|
|
|
|
| 306 |
|
| 307 |
async def test_resource_decorator_incorrect_usage(self):
|
| 308 |
mcp = FastMCP()
|
|
|
|
| 329 |
|
| 330 |
async with Client(mcp) as client:
|
| 331 |
result = await client.read_resource("resource://data")
|
| 332 |
+
assert result[0].text == "Hello, world!" # type: ignore[attr-defined]
|
|
|
|
| 333 |
|
| 334 |
async def test_resource_decorator_with_description(self):
|
| 335 |
mcp = FastMCP()
|
|
|
|
| 373 |
|
| 374 |
async with Client(mcp) as client:
|
| 375 |
result = await client.read_resource("resource://data")
|
| 376 |
+
assert result[0].text == "My prefix: Hello, world!" # type: ignore[attr-defined]
|
|
|
|
| 377 |
|
| 378 |
async def test_resource_decorator_classmethod(self):
|
| 379 |
mcp = FastMCP()
|
|
|
|
| 391 |
|
| 392 |
async with Client(mcp) as client:
|
| 393 |
result = await client.read_resource("resource://data")
|
| 394 |
+
assert result[0].text == "Class prefix: Hello, world!" # type: ignore[attr-defined]
|
|
|
|
| 395 |
|
| 396 |
async def test_resource_decorator_staticmethod(self):
|
| 397 |
mcp = FastMCP()
|
|
|
|
| 404 |
|
| 405 |
async with Client(mcp) as client:
|
| 406 |
result = await client.read_resource("resource://data")
|
| 407 |
+
assert result[0].text == "Static Hello, world!" # type: ignore[attr-defined]
|
|
|
|
| 408 |
|
| 409 |
async def test_resource_decorator_async_function(self):
|
| 410 |
mcp = FastMCP()
|
|
|
|
| 415 |
|
| 416 |
async with Client(mcp) as client:
|
| 417 |
result = await client.read_resource("resource://data")
|
| 418 |
+
assert result[0].text == "Async Hello, world!" # type: ignore[attr-defined]
|
|
|
|
| 419 |
|
| 420 |
|
| 421 |
class TestTemplateDecorator:
|
|
|
|
| 434 |
|
| 435 |
async with Client(mcp) as client:
|
| 436 |
result = await client.read_resource("resource://test/data")
|
| 437 |
+
assert result[0].text == "Data for test" # type: ignore[attr-defined]
|
|
|
|
| 438 |
|
| 439 |
async def test_template_decorator_incorrect_usage(self):
|
| 440 |
mcp = FastMCP()
|
|
|
|
| 461 |
|
| 462 |
async with Client(mcp) as client:
|
| 463 |
result = await client.read_resource("resource://test/data")
|
| 464 |
+
assert result[0].text == "Data for test" # type: ignore[attr-defined]
|
|
|
|
| 465 |
|
| 466 |
async def test_template_decorator_with_description(self):
|
| 467 |
mcp = FastMCP()
|
|
|
|
| 492 |
|
| 493 |
async with Client(mcp) as client:
|
| 494 |
result = await client.read_resource("resource://test/data")
|
| 495 |
+
assert result[0].text == "My prefix: Data for test" # type: ignore[attr-defined]
|
|
|
|
| 496 |
|
| 497 |
async def test_template_decorator_classmethod(self):
|
| 498 |
mcp = FastMCP()
|
|
|
|
| 512 |
|
| 513 |
async with Client(mcp) as client:
|
| 514 |
result = await client.read_resource("resource://test/data")
|
| 515 |
+
assert result[0].text == "Class prefix: Data for test" # type: ignore[attr-defined]
|
|
|
|
| 516 |
|
| 517 |
async def test_template_decorator_staticmethod(self):
|
| 518 |
mcp = FastMCP()
|
|
|
|
| 525 |
|
| 526 |
async with Client(mcp) as client:
|
| 527 |
result = await client.read_resource("resource://test/data")
|
| 528 |
+
assert result[0].text == "Static Data for test" # type: ignore[attr-defined]
|
|
|
|
| 529 |
|
| 530 |
async def test_template_decorator_async_function(self):
|
| 531 |
mcp = FastMCP()
|
|
|
|
| 536 |
|
| 537 |
async with Client(mcp) as client:
|
| 538 |
result = await client.read_resource("resource://test/data")
|
| 539 |
+
assert result[0].text == "Async Data for test" # type: ignore[attr-defined]
|
|
|
|
| 540 |
|
| 541 |
async def test_template_decorator_with_tags(self):
|
| 542 |
"""Test that the template decorator properly sets tags."""
|
|
|
|
| 577 |
assert prompt.name == "fn"
|
| 578 |
# Don't compare functions directly since validate_call wraps them
|
| 579 |
content = await prompt.render()
|
| 580 |
+
assert content[0].content.text == "Hello, world!" # type: ignore[attr-defined]
|
|
|
|
| 581 |
|
| 582 |
async def test_prompt_decorator_incorrect_usage(self):
|
| 583 |
mcp = FastMCP()
|
|
|
|
| 602 |
prompt = prompts_dict["custom_name"]
|
| 603 |
assert prompt.name == "custom_name"
|
| 604 |
content = await prompt.render()
|
| 605 |
+
assert content[0].content.text == "Hello, world!" # type: ignore[attr-defined]
|
|
|
|
| 606 |
|
| 607 |
async def test_prompt_decorator_with_description(self):
|
| 608 |
mcp = FastMCP()
|
|
|
|
| 616 |
prompt = prompts_dict["fn"]
|
| 617 |
assert prompt.description == "A custom description"
|
| 618 |
content = await prompt.render()
|
| 619 |
+
assert content[0].content.text == "Hello, world!" # type: ignore[attr-defined]
|
|
|
|
| 620 |
|
| 621 |
async def test_prompt_decorator_with_parameters(self):
|
| 622 |
mcp = FastMCP()
|
|
|
|
| 639 |
result = await client.get_prompt("test_prompt", {"name": "World"})
|
| 640 |
assert len(result.messages) == 1
|
| 641 |
message = result.messages[0]
|
| 642 |
+
assert message.content.text == "Hello, World!" # type: ignore[attr-defined]
|
|
|
|
| 643 |
|
| 644 |
result = await client.get_prompt(
|
| 645 |
"test_prompt", {"name": "World", "greeting": "Hi"}
|
| 646 |
)
|
| 647 |
assert len(result.messages) == 1
|
| 648 |
message = result.messages[0]
|
| 649 |
+
assert message.content.text == "Hi, World!" # type: ignore[attr-defined]
|
|
|
|
| 650 |
|
| 651 |
async def test_prompt_decorator_instance_method(self):
|
| 652 |
mcp = FastMCP()
|
|
|
|
| 665 |
result = await client.get_prompt("test_prompt")
|
| 666 |
assert len(result.messages) == 1
|
| 667 |
message = result.messages[0]
|
| 668 |
+
assert message.content.text == "My prefix: Hello, world!" # type: ignore[attr-defined]
|
|
|
|
| 669 |
|
| 670 |
async def test_prompt_decorator_classmethod(self):
|
| 671 |
mcp = FastMCP()
|
|
|
|
| 683 |
result = await client.get_prompt("test_prompt")
|
| 684 |
assert len(result.messages) == 1
|
| 685 |
message = result.messages[0]
|
| 686 |
+
assert message.content.text == "Class prefix: Hello, world!" # type: ignore[attr-defined]
|
|
|
|
| 687 |
|
| 688 |
async def test_prompt_decorator_staticmethod(self):
|
| 689 |
mcp = FastMCP()
|
|
|
|
| 698 |
result = await client.get_prompt("test_prompt")
|
| 699 |
assert len(result.messages) == 1
|
| 700 |
message = result.messages[0]
|
| 701 |
+
assert message.content.text == "Static Hello, world!" # type: ignore[attr-defined]
|
|
|
|
| 702 |
|
| 703 |
async def test_prompt_decorator_async_function(self):
|
| 704 |
mcp = FastMCP()
|
|
|
|
| 711 |
result = await client.get_prompt("test_prompt")
|
| 712 |
assert len(result.messages) == 1
|
| 713 |
message = result.messages[0]
|
| 714 |
+
assert message.content.text == "Async Hello, world!" # type: ignore[attr-defined]
|
|
|
|
| 715 |
|
| 716 |
async def test_prompt_decorator_with_tags(self):
|
| 717 |
"""Test that the prompt decorator properly sets tags."""
|
|
|
|
| 908 |
async with Client(main_server) as client:
|
| 909 |
# Regular resource
|
| 910 |
result = await client.read_resource("resource://prefix/test-resource")
|
| 911 |
+
assert result[0].text == "Resource content" # type: ignore[attr-defined]
|
|
|
|
| 912 |
|
| 913 |
# Absolute path resource
|
| 914 |
result = await client.read_resource("resource://prefix//absolute/path")
|
| 915 |
+
assert result[0].text == "Absolute resource content" # type: ignore[attr-defined]
|
|
|
|
| 916 |
|
| 917 |
# Template resource
|
| 918 |
result = await client.read_resource(
|
| 919 |
"resource://prefix/param-value/template"
|
| 920 |
)
|
| 921 |
+
assert result[0].text == "Template resource with param-value" # type: ignore[attr-defined]
|
|
|
|
| 922 |
|
| 923 |
@pytest.mark.parametrize(
|
| 924 |
"uri,prefix,expected_match,expected_strip",
|
|
|
|
| 994 |
# Verify we can access the resources
|
| 995 |
async with Client(target_server) as client:
|
| 996 |
result = await client.read_resource("resource://imported/test-resource")
|
| 997 |
+
assert result[0].text == "Resource content" # type: ignore[attr-defined]
|
|
|
|
| 998 |
|
| 999 |
result = await client.read_resource("resource://imported//absolute/path")
|
| 1000 |
+
assert result[0].text == "Absolute resource content" # type: ignore[attr-defined]
|
|
|
|
| 1001 |
|
| 1002 |
result = await client.read_resource(
|
| 1003 |
"resource://imported/param-value/template"
|
| 1004 |
)
|
| 1005 |
+
assert result[0].text == "Template resource with param-value" # type: ignore[attr-defined]
|
|
|
tests/server/test_server_interactions.py
CHANGED
|
@@ -10,7 +10,6 @@ import pydantic_core
|
|
| 10 |
import pytest
|
| 11 |
from mcp import McpError
|
| 12 |
from mcp.types import (
|
| 13 |
-
BlobResourceContents,
|
| 14 |
ImageContent,
|
| 15 |
TextContent,
|
| 16 |
TextResourceContents,
|
|
@@ -77,14 +76,12 @@ class TestTools:
|
|
| 77 |
async def test_call_tool(self, tool_server: FastMCP):
|
| 78 |
async with Client(tool_server) as client:
|
| 79 |
result = await client.call_tool("add", {"x": 1, "y": 2})
|
| 80 |
-
assert
|
| 81 |
-
assert result[0].text == "3"
|
| 82 |
|
| 83 |
async def test_call_tool_as_client(self, tool_server: FastMCP):
|
| 84 |
async with Client(tool_server) as client:
|
| 85 |
result = await client.call_tool("add", {"x": 1, "y": 2})
|
| 86 |
-
assert
|
| 87 |
-
assert result[0].text == "3"
|
| 88 |
|
| 89 |
async def test_call_tool_error(self, tool_server: FastMCP):
|
| 90 |
async with Client(tool_server) as client:
|
|
@@ -113,8 +110,7 @@ class TestTools:
|
|
| 113 |
async def test_tool_returns_list(self, tool_server: FastMCP):
|
| 114 |
async with Client(tool_server) as client:
|
| 115 |
result = await client.call_tool("list_tool", {})
|
| 116 |
-
assert
|
| 117 |
-
assert result[0].text == '[\n "x",\n 2\n]'
|
| 118 |
|
| 119 |
|
| 120 |
class TestToolReturnTypes:
|
|
@@ -127,8 +123,7 @@ class TestToolReturnTypes:
|
|
| 127 |
|
| 128 |
async with Client(mcp) as client:
|
| 129 |
result = await client.call_tool("string_tool", {})
|
| 130 |
-
assert
|
| 131 |
-
assert result[0].text == "Hello, world!"
|
| 132 |
|
| 133 |
async def test_bytes(self, tmp_path: Path):
|
| 134 |
mcp = FastMCP()
|
|
@@ -139,8 +134,7 @@ class TestToolReturnTypes:
|
|
| 139 |
|
| 140 |
async with Client(mcp) as client:
|
| 141 |
result = await client.call_tool("bytes_tool", {})
|
| 142 |
-
assert
|
| 143 |
-
assert result[0].text == '"Hello, world!"'
|
| 144 |
|
| 145 |
async def test_uuid(self):
|
| 146 |
mcp = FastMCP()
|
|
@@ -153,8 +147,7 @@ class TestToolReturnTypes:
|
|
| 153 |
|
| 154 |
async with Client(mcp) as client:
|
| 155 |
result = await client.call_tool("uuid_tool", {})
|
| 156 |
-
assert
|
| 157 |
-
assert result[0].text == pydantic_core.to_json(test_uuid).decode()
|
| 158 |
|
| 159 |
async def test_path(self):
|
| 160 |
mcp = FastMCP()
|
|
@@ -167,8 +160,7 @@ class TestToolReturnTypes:
|
|
| 167 |
|
| 168 |
async with Client(mcp) as client:
|
| 169 |
result = await client.call_tool("path_tool", {})
|
| 170 |
-
assert
|
| 171 |
-
assert result[0].text == pydantic_core.to_json(test_path).decode()
|
| 172 |
|
| 173 |
async def test_datetime(self):
|
| 174 |
mcp = FastMCP()
|
|
@@ -181,8 +173,7 @@ class TestToolReturnTypes:
|
|
| 181 |
|
| 182 |
async with Client(mcp) as client:
|
| 183 |
result = await client.call_tool("datetime_tool", {})
|
| 184 |
-
assert
|
| 185 |
-
assert result[0].text == pydantic_core.to_json(dt).decode()
|
| 186 |
|
| 187 |
async def test_image(self, tmp_path: Path):
|
| 188 |
mcp = FastMCP()
|
|
@@ -337,8 +328,7 @@ class TestToolParameters:
|
|
| 337 |
async with Client(mcp) as client:
|
| 338 |
# String with integer value should be coerced to int
|
| 339 |
result = await client.call_tool("add_one", {"x": "42"})
|
| 340 |
-
assert
|
| 341 |
-
assert result[0].text == "43"
|
| 342 |
|
| 343 |
async def test_tool_bool_coercion(self):
|
| 344 |
"""Test string-to-bool type coercion."""
|
|
@@ -351,12 +341,10 @@ class TestToolParameters:
|
|
| 351 |
async with Client(mcp) as client:
|
| 352 |
# String with boolean value should be coerced to bool
|
| 353 |
result = await client.call_tool("toggle", {"flag": "true"})
|
| 354 |
-
assert
|
| 355 |
-
assert result[0].text == "false"
|
| 356 |
|
| 357 |
result = await client.call_tool("toggle", {"flag": "false"})
|
| 358 |
-
assert
|
| 359 |
-
assert result[0].text == "true"
|
| 360 |
|
| 361 |
async def test_annotated_field_validation(self):
|
| 362 |
mcp = FastMCP()
|
|
@@ -411,8 +399,7 @@ class TestToolParameters:
|
|
| 411 |
|
| 412 |
async with Client(mcp) as client:
|
| 413 |
result = await client.call_tool("analyze", {"x": "a"})
|
| 414 |
-
assert
|
| 415 |
-
assert result[0].text == "a"
|
| 416 |
|
| 417 |
async def test_enum_type_validation_error(self):
|
| 418 |
mcp = FastMCP()
|
|
@@ -444,8 +431,7 @@ class TestToolParameters:
|
|
| 444 |
|
| 445 |
async with Client(mcp) as client:
|
| 446 |
result = await client.call_tool("analyze", {"x": "red"})
|
| 447 |
-
assert
|
| 448 |
-
assert result[0].text == "red"
|
| 449 |
|
| 450 |
async def test_union_type_validation(self):
|
| 451 |
mcp = FastMCP()
|
|
@@ -456,12 +442,10 @@ class TestToolParameters:
|
|
| 456 |
|
| 457 |
async with Client(mcp) as client:
|
| 458 |
result = await client.call_tool("analyze", {"x": 1})
|
| 459 |
-
assert
|
| 460 |
-
assert result[0].text == "1"
|
| 461 |
|
| 462 |
result = await client.call_tool("analyze", {"x": 1.0})
|
| 463 |
-
assert
|
| 464 |
-
assert result[0].text == "1.0"
|
| 465 |
|
| 466 |
with pytest.raises(ToolError, match="Error calling tool 'analyze'"):
|
| 467 |
await client.call_tool("analyze", {"x": "not a number"})
|
|
@@ -479,8 +463,7 @@ class TestToolParameters:
|
|
| 479 |
|
| 480 |
async with Client(mcp) as client:
|
| 481 |
result = await client.call_tool("send_path", {"path": str(test_path)})
|
| 482 |
-
assert
|
| 483 |
-
assert result[0].text == str(test_path)
|
| 484 |
|
| 485 |
async def test_path_type_error(self):
|
| 486 |
mcp = FastMCP()
|
|
@@ -505,8 +488,7 @@ class TestToolParameters:
|
|
| 505 |
|
| 506 |
async with Client(mcp) as client:
|
| 507 |
result = await client.call_tool("send_uuid", {"x": test_uuid})
|
| 508 |
-
assert
|
| 509 |
-
assert result[0].text == str(test_uuid)
|
| 510 |
|
| 511 |
async def test_uuid_type_error(self):
|
| 512 |
mcp = FastMCP()
|
|
@@ -530,8 +512,7 @@ class TestToolParameters:
|
|
| 530 |
|
| 531 |
async with Client(mcp) as client:
|
| 532 |
result = await client.call_tool("send_datetime", {"x": dt})
|
| 533 |
-
assert
|
| 534 |
-
assert result[0].text == dt.isoformat()
|
| 535 |
|
| 536 |
async def test_datetime_type_parse_string(self):
|
| 537 |
mcp = FastMCP()
|
|
@@ -544,8 +525,7 @@ class TestToolParameters:
|
|
| 544 |
result = await client.call_tool(
|
| 545 |
"send_datetime", {"x": "2021-01-01T00:00:00"}
|
| 546 |
)
|
| 547 |
-
assert
|
| 548 |
-
assert result[0].text == "2021-01-01T00:00:00"
|
| 549 |
|
| 550 |
async def test_datetime_type_error(self):
|
| 551 |
mcp = FastMCP()
|
|
@@ -567,8 +547,7 @@ class TestToolParameters:
|
|
| 567 |
|
| 568 |
async with Client(mcp) as client:
|
| 569 |
result = await client.call_tool("send_date", {"x": datetime.date.today()})
|
| 570 |
-
assert
|
| 571 |
-
assert result[0].text == datetime.date.today().isoformat()
|
| 572 |
|
| 573 |
async def test_date_type_parse_string(self):
|
| 574 |
mcp = FastMCP()
|
|
@@ -579,8 +558,7 @@ class TestToolParameters:
|
|
| 579 |
|
| 580 |
async with Client(mcp) as client:
|
| 581 |
result = await client.call_tool("send_date", {"x": "2021-01-01"})
|
| 582 |
-
assert
|
| 583 |
-
assert result[0].text == "2021-01-01"
|
| 584 |
|
| 585 |
async def test_timedelta_type(self):
|
| 586 |
mcp = FastMCP()
|
|
@@ -593,8 +571,7 @@ class TestToolParameters:
|
|
| 593 |
result = await client.call_tool(
|
| 594 |
"send_timedelta", {"x": datetime.timedelta(days=1)}
|
| 595 |
)
|
| 596 |
-
assert
|
| 597 |
-
assert result[0].text == "1 day, 0:00:00"
|
| 598 |
|
| 599 |
async def test_timedelta_type_parse_int(self):
|
| 600 |
mcp = FastMCP()
|
|
@@ -605,8 +582,7 @@ class TestToolParameters:
|
|
| 605 |
|
| 606 |
async with Client(mcp) as client:
|
| 607 |
result = await client.call_tool("send_timedelta", {"x": 1000})
|
| 608 |
-
assert
|
| 609 |
-
assert result[0].text == "0:16:40"
|
| 610 |
|
| 611 |
|
| 612 |
class TestToolContextInjection:
|
|
@@ -639,7 +615,7 @@ class TestToolContextInjection:
|
|
| 639 |
result = await client.call_tool("tool_with_context", {"x": 42})
|
| 640 |
assert len(result) == 1
|
| 641 |
content = result[0]
|
| 642 |
-
assert
|
| 643 |
|
| 644 |
async def test_async_context(self):
|
| 645 |
"""Test that context works in async functions."""
|
|
@@ -654,8 +630,7 @@ class TestToolContextInjection:
|
|
| 654 |
result = await client.call_tool("async_tool", {"x": 42})
|
| 655 |
assert len(result) == 1
|
| 656 |
content = result[0]
|
| 657 |
-
assert
|
| 658 |
-
assert content.text == "Async request 2: 42"
|
| 659 |
|
| 660 |
async def test_optional_context(self):
|
| 661 |
"""Test that context is optional."""
|
|
@@ -669,8 +644,7 @@ class TestToolContextInjection:
|
|
| 669 |
result = await client.call_tool("no_context", {"x": 21})
|
| 670 |
assert len(result) == 1
|
| 671 |
content = result[0]
|
| 672 |
-
assert
|
| 673 |
-
assert content.text == "42"
|
| 674 |
|
| 675 |
async def test_context_resource_access(self):
|
| 676 |
"""Test that context can access resources."""
|
|
@@ -692,8 +666,7 @@ class TestToolContextInjection:
|
|
| 692 |
result = await client.call_tool("tool_with_resource", {})
|
| 693 |
assert len(result) == 1
|
| 694 |
content = result[0]
|
| 695 |
-
assert
|
| 696 |
-
assert "Read resource: resource data" in content.text
|
| 697 |
|
| 698 |
async def test_tool_decorator_with_tags(self):
|
| 699 |
"""Test that the tool decorator properly sets tags."""
|
|
@@ -721,8 +694,7 @@ class TestToolContextInjection:
|
|
| 721 |
|
| 722 |
async with Client(mcp) as client:
|
| 723 |
result = await client.call_tool("MyTool", {"x": 2})
|
| 724 |
-
assert
|
| 725 |
-
assert result[0].text == "4"
|
| 726 |
|
| 727 |
|
| 728 |
class TestResource:
|
|
@@ -739,8 +711,7 @@ class TestResource:
|
|
| 739 |
|
| 740 |
async with Client(mcp) as client:
|
| 741 |
result = await client.read_resource(AnyUrl("resource://test"))
|
| 742 |
-
assert
|
| 743 |
-
assert result[0].text == "Hello, world!"
|
| 744 |
|
| 745 |
async def test_binary_resource(self):
|
| 746 |
mcp = FastMCP()
|
|
@@ -758,8 +729,7 @@ class TestResource:
|
|
| 758 |
|
| 759 |
async with Client(mcp) as client:
|
| 760 |
result = await client.read_resource(AnyUrl("resource://binary"))
|
| 761 |
-
assert
|
| 762 |
-
assert result[0].blob == base64.b64encode(b"Binary data").decode()
|
| 763 |
|
| 764 |
async def test_file_resource_text(self, tmp_path: Path):
|
| 765 |
mcp = FastMCP()
|
|
@@ -775,8 +745,7 @@ class TestResource:
|
|
| 775 |
|
| 776 |
async with Client(mcp) as client:
|
| 777 |
result = await client.read_resource(AnyUrl("file://test.txt"))
|
| 778 |
-
assert
|
| 779 |
-
assert result[0].text == "Hello from file!"
|
| 780 |
|
| 781 |
async def test_file_resource_binary(self, tmp_path: Path):
|
| 782 |
mcp = FastMCP()
|
|
@@ -795,8 +764,7 @@ class TestResource:
|
|
| 795 |
|
| 796 |
async with Client(mcp) as client:
|
| 797 |
result = await client.read_resource(AnyUrl("file://test.bin"))
|
| 798 |
-
assert
|
| 799 |
-
assert result[0].blob == base64.b64encode(b"Binary file data").decode()
|
| 800 |
|
| 801 |
|
| 802 |
class TestResourceContext:
|
|
@@ -810,8 +778,7 @@ class TestResourceContext:
|
|
| 810 |
|
| 811 |
async with Client(mcp) as client:
|
| 812 |
result = await client.read_resource(AnyUrl("resource://test"))
|
| 813 |
-
assert
|
| 814 |
-
assert result[0].text == "2"
|
| 815 |
|
| 816 |
|
| 817 |
class TestResourceTemplates:
|
|
@@ -860,8 +827,7 @@ class TestResourceTemplates:
|
|
| 860 |
|
| 861 |
async with Client(mcp) as client:
|
| 862 |
result = await client.read_resource(AnyUrl("resource://test/data"))
|
| 863 |
-
assert
|
| 864 |
-
assert result[0].text == "Data for test"
|
| 865 |
|
| 866 |
async def test_resource_mismatched_params(self):
|
| 867 |
"""Test that mismatched parameters raise an error"""
|
|
@@ -888,8 +854,7 @@ class TestResourceTemplates:
|
|
| 888 |
result = await client.read_resource(
|
| 889 |
AnyUrl("resource://cursor/fastmcp/data")
|
| 890 |
)
|
| 891 |
-
assert
|
| 892 |
-
assert result[0].text == "Data for cursor/fastmcp"
|
| 893 |
|
| 894 |
async def test_resource_multiple_mismatched_params(self):
|
| 895 |
"""Test that mismatched parameters raise an error"""
|
|
@@ -913,8 +878,7 @@ class TestResourceTemplates:
|
|
| 913 |
|
| 914 |
async with Client(mcp) as client:
|
| 915 |
result = await client.read_resource(AnyUrl("resource://static"))
|
| 916 |
-
assert
|
| 917 |
-
assert result[0].text == "Static data"
|
| 918 |
|
| 919 |
async def test_template_with_varkwargs(self):
|
| 920 |
"""Test that a template can have **kwargs."""
|
|
@@ -926,8 +890,7 @@ class TestResourceTemplates:
|
|
| 926 |
|
| 927 |
async with Client(mcp) as client:
|
| 928 |
result = await client.read_resource(AnyUrl("test://1/2/3"))
|
| 929 |
-
assert
|
| 930 |
-
assert result[0].text == "6"
|
| 931 |
|
| 932 |
async def test_template_with_default_params(self):
|
| 933 |
"""Test that a template can have default parameters."""
|
|
@@ -946,13 +909,11 @@ class TestResourceTemplates:
|
|
| 946 |
# Call the template and verify it uses the default value
|
| 947 |
async with Client(mcp) as client:
|
| 948 |
result = await client.read_resource(AnyUrl("math://add/5"))
|
| 949 |
-
assert
|
| 950 |
-
assert result[0].text == "15" # 5 + default 10
|
| 951 |
|
| 952 |
# Can also call with explicit params
|
| 953 |
result2 = await client.read_resource(AnyUrl("math://add/7"))
|
| 954 |
-
assert
|
| 955 |
-
assert result2[0].text == "17" # 7 + default 10
|
| 956 |
|
| 957 |
async def test_template_to_resource_conversion(self):
|
| 958 |
"""Test that a template can be converted to a resource."""
|
|
@@ -971,8 +932,7 @@ class TestResourceTemplates:
|
|
| 971 |
# When accessed, should create a concrete resource
|
| 972 |
async with Client(mcp) as client:
|
| 973 |
result = await client.read_resource(AnyUrl("resource://test/data"))
|
| 974 |
-
assert
|
| 975 |
-
assert result[0].text == "Data for test"
|
| 976 |
|
| 977 |
async def test_stacked_resource_template_decorators(self):
|
| 978 |
"""Test that resource template decorators can be stacked."""
|
|
@@ -1011,15 +971,15 @@ class TestResourceTemplates:
|
|
| 1011 |
email_result = await client.read_resource(
|
| 1012 |
AnyUrl("users://email/user@example.com")
|
| 1013 |
)
|
| 1014 |
-
assert
|
| 1015 |
-
email_data = json.loads(email_result[0].text)
|
| 1016 |
assert email_data["lookup"] == "email"
|
| 1017 |
assert email_data["email"] == "user@example.com"
|
| 1018 |
|
| 1019 |
# Test lookup by name
|
| 1020 |
name_result = await client.read_resource(AnyUrl("users://name/John"))
|
| 1021 |
-
assert
|
| 1022 |
-
|
| 1023 |
assert name_data["lookup"] == "name"
|
| 1024 |
assert name_data["name"] == "John"
|
| 1025 |
assert name_data["email"] == "dummy@example.com"
|
|
@@ -1044,8 +1004,7 @@ class TestResourceTemplates:
|
|
| 1044 |
|
| 1045 |
async with Client(mcp) as client:
|
| 1046 |
result = await client.read_resource(AnyUrl("resource://test/data"))
|
| 1047 |
-
assert
|
| 1048 |
-
assert result[0].text == "Template resource: test/data"
|
| 1049 |
|
| 1050 |
async def test_templates_match_in_order_of_definition(self):
|
| 1051 |
"""
|
|
@@ -1065,12 +1024,10 @@ class TestResourceTemplates:
|
|
| 1065 |
|
| 1066 |
async with Client(mcp) as client:
|
| 1067 |
result = await client.read_resource(AnyUrl("resource://a/b/c"))
|
| 1068 |
-
assert
|
| 1069 |
-
assert result[0].text == "Template resource 1: a/b/c"
|
| 1070 |
|
| 1071 |
result = await client.read_resource(AnyUrl("resource://a/b"))
|
| 1072 |
-
assert
|
| 1073 |
-
assert result[0].text == "Template resource 1: a/b"
|
| 1074 |
|
| 1075 |
async def test_templates_shadow_each_other_reorder(self):
|
| 1076 |
"""
|
|
@@ -1089,12 +1046,10 @@ class TestResourceTemplates:
|
|
| 1089 |
|
| 1090 |
async with Client(mcp) as client:
|
| 1091 |
result = await client.read_resource(AnyUrl("resource://a/b/c"))
|
| 1092 |
-
assert
|
| 1093 |
-
assert result[0].text == "Template resource 2: a/b/c"
|
| 1094 |
|
| 1095 |
result = await client.read_resource(AnyUrl("resource://a/b"))
|
| 1096 |
-
assert
|
| 1097 |
-
assert result[0].text == "Template resource 1: a/b"
|
| 1098 |
|
| 1099 |
|
| 1100 |
class TestResourceTemplateContext:
|
|
@@ -1108,8 +1063,7 @@ class TestResourceTemplateContext:
|
|
| 1108 |
|
| 1109 |
async with Client(mcp) as client:
|
| 1110 |
result = await client.read_resource(AnyUrl("resource://test"))
|
| 1111 |
-
assert
|
| 1112 |
-
assert result[0].text.startswith("Resource template: test 2")
|
| 1113 |
|
| 1114 |
async def test_resource_template_context_with_callable_object(self):
|
| 1115 |
mcp = FastMCP()
|
|
@@ -1122,8 +1076,7 @@ class TestResourceTemplateContext:
|
|
| 1122 |
|
| 1123 |
async with Client(mcp) as client:
|
| 1124 |
result = await client.read_resource(AnyUrl("resource://test"))
|
| 1125 |
-
assert
|
| 1126 |
-
assert result[0].text.startswith("Resource template: test 2")
|
| 1127 |
|
| 1128 |
|
| 1129 |
class TestPrompts:
|
|
@@ -1143,8 +1096,7 @@ class TestPrompts:
|
|
| 1143 |
assert prompt.name == "fn"
|
| 1144 |
# Don't compare functions directly since validate_call wraps them
|
| 1145 |
content = await prompt.render()
|
| 1146 |
-
assert
|
| 1147 |
-
assert content[0].content.text == "Hello, world!"
|
| 1148 |
|
| 1149 |
async def test_prompt_decorator_with_name(self):
|
| 1150 |
"""Test prompt decorator with custom name."""
|
|
@@ -1159,8 +1111,7 @@ class TestPrompts:
|
|
| 1159 |
prompt = prompts_dict["custom_name"]
|
| 1160 |
assert prompt.name == "custom_name"
|
| 1161 |
content = await prompt.render()
|
| 1162 |
-
assert
|
| 1163 |
-
assert content[0].content.text == "Hello, world!"
|
| 1164 |
|
| 1165 |
async def test_prompt_decorator_with_description(self):
|
| 1166 |
"""Test prompt decorator with custom description."""
|
|
@@ -1175,8 +1126,7 @@ class TestPrompts:
|
|
| 1175 |
prompt = prompts_dict["fn"]
|
| 1176 |
assert prompt.description == "A custom description"
|
| 1177 |
content = await prompt.render()
|
| 1178 |
-
assert
|
| 1179 |
-
assert content[0].content.text == "Hello, world!"
|
| 1180 |
|
| 1181 |
def test_prompt_decorator_error(self):
|
| 1182 |
"""Test error when decorator is used incorrectly."""
|
|
@@ -1224,8 +1174,7 @@ class TestPrompts:
|
|
| 1224 |
message = result.messages[0]
|
| 1225 |
assert message.role == "user"
|
| 1226 |
content = message.content
|
| 1227 |
-
assert
|
| 1228 |
-
assert content.text == "Hello, World!"
|
| 1229 |
|
| 1230 |
async def test_get_prompt_with_resource(self):
|
| 1231 |
"""Test getting a prompt that returns resource content."""
|
|
@@ -1249,10 +1198,10 @@ class TestPrompts:
|
|
| 1249 |
result = await client.get_prompt("fn")
|
| 1250 |
assert result.messages[0].role == "user"
|
| 1251 |
content = result.messages[0].content
|
| 1252 |
-
assert isinstance(content, EmbeddedResource)
|
| 1253 |
resource = content.resource
|
| 1254 |
-
assert isinstance(resource, TextResourceContents)
|
| 1255 |
-
assert resource.text == "File contents"
|
| 1256 |
assert resource.mimeType == "text/plain"
|
| 1257 |
|
| 1258 |
async def test_get_unknown_prompt(self):
|
|
@@ -1342,5 +1291,4 @@ class TestPromptContext:
|
|
| 1342 |
assert len(result.messages) == 1
|
| 1343 |
message = result.messages[0]
|
| 1344 |
assert message.role == "user"
|
| 1345 |
-
assert
|
| 1346 |
-
assert message.content.text == "Hello, World! 2"
|
|
|
|
| 10 |
import pytest
|
| 11 |
from mcp import McpError
|
| 12 |
from mcp.types import (
|
|
|
|
| 13 |
ImageContent,
|
| 14 |
TextContent,
|
| 15 |
TextResourceContents,
|
|
|
|
| 76 |
async def test_call_tool(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 result[0].text == "3" # type: ignore[attr-defined]
|
|
|
|
| 80 |
|
| 81 |
async def test_call_tool_as_client(self, tool_server: FastMCP):
|
| 82 |
async with Client(tool_server) as client:
|
| 83 |
result = await client.call_tool("add", {"x": 1, "y": 2})
|
| 84 |
+
assert result[0].text == "3" # type: ignore[attr-defined]
|
|
|
|
| 85 |
|
| 86 |
async def test_call_tool_error(self, tool_server: FastMCP):
|
| 87 |
async with Client(tool_server) as client:
|
|
|
|
| 110 |
async def test_tool_returns_list(self, tool_server: FastMCP):
|
| 111 |
async with Client(tool_server) as client:
|
| 112 |
result = await client.call_tool("list_tool", {})
|
| 113 |
+
assert result[0].text == '[\n "x",\n 2\n]' # type: ignore[attr-defined]
|
|
|
|
| 114 |
|
| 115 |
|
| 116 |
class TestToolReturnTypes:
|
|
|
|
| 123 |
|
| 124 |
async with Client(mcp) as client:
|
| 125 |
result = await client.call_tool("string_tool", {})
|
| 126 |
+
assert result[0].text == "Hello, world!" # type: ignore[attr-defined]
|
|
|
|
| 127 |
|
| 128 |
async def test_bytes(self, tmp_path: Path):
|
| 129 |
mcp = FastMCP()
|
|
|
|
| 134 |
|
| 135 |
async with Client(mcp) as client:
|
| 136 |
result = await client.call_tool("bytes_tool", {})
|
| 137 |
+
assert result[0].text == '"Hello, world!"' # type: ignore[attr-defined]
|
|
|
|
| 138 |
|
| 139 |
async def test_uuid(self):
|
| 140 |
mcp = FastMCP()
|
|
|
|
| 147 |
|
| 148 |
async with Client(mcp) as client:
|
| 149 |
result = await client.call_tool("uuid_tool", {})
|
| 150 |
+
assert result[0].text == pydantic_core.to_json(test_uuid).decode() # type: ignore[attr-defined]
|
|
|
|
| 151 |
|
| 152 |
async def test_path(self):
|
| 153 |
mcp = FastMCP()
|
|
|
|
| 160 |
|
| 161 |
async with Client(mcp) as client:
|
| 162 |
result = await client.call_tool("path_tool", {})
|
| 163 |
+
assert result[0].text == pydantic_core.to_json(test_path).decode() # type: ignore[attr-defined]
|
|
|
|
| 164 |
|
| 165 |
async def test_datetime(self):
|
| 166 |
mcp = FastMCP()
|
|
|
|
| 173 |
|
| 174 |
async with Client(mcp) as client:
|
| 175 |
result = await client.call_tool("datetime_tool", {})
|
| 176 |
+
assert result[0].text == pydantic_core.to_json(dt).decode() # type: ignore[attr-defined]
|
|
|
|
| 177 |
|
| 178 |
async def test_image(self, tmp_path: Path):
|
| 179 |
mcp = FastMCP()
|
|
|
|
| 328 |
async with Client(mcp) as client:
|
| 329 |
# String with integer value should be coerced to int
|
| 330 |
result = await client.call_tool("add_one", {"x": "42"})
|
| 331 |
+
assert result[0].text == "43" # type: ignore[attr-defined]
|
|
|
|
| 332 |
|
| 333 |
async def test_tool_bool_coercion(self):
|
| 334 |
"""Test string-to-bool type coercion."""
|
|
|
|
| 341 |
async with Client(mcp) as client:
|
| 342 |
# String with boolean value should be coerced to bool
|
| 343 |
result = await client.call_tool("toggle", {"flag": "true"})
|
| 344 |
+
assert result[0].text == "false" # type: ignore[attr-defined]
|
|
|
|
| 345 |
|
| 346 |
result = await client.call_tool("toggle", {"flag": "false"})
|
| 347 |
+
assert result[0].text == "true" # type: ignore[attr-defined]
|
|
|
|
| 348 |
|
| 349 |
async def test_annotated_field_validation(self):
|
| 350 |
mcp = FastMCP()
|
|
|
|
| 399 |
|
| 400 |
async with Client(mcp) as client:
|
| 401 |
result = await client.call_tool("analyze", {"x": "a"})
|
| 402 |
+
assert result[0].text == "a" # type: ignore[attr-defined]
|
|
|
|
| 403 |
|
| 404 |
async def test_enum_type_validation_error(self):
|
| 405 |
mcp = FastMCP()
|
|
|
|
| 431 |
|
| 432 |
async with Client(mcp) as client:
|
| 433 |
result = await client.call_tool("analyze", {"x": "red"})
|
| 434 |
+
assert result[0].text == "red" # type: ignore[attr-defined]
|
|
|
|
| 435 |
|
| 436 |
async def test_union_type_validation(self):
|
| 437 |
mcp = FastMCP()
|
|
|
|
| 442 |
|
| 443 |
async with Client(mcp) as client:
|
| 444 |
result = await client.call_tool("analyze", {"x": 1})
|
| 445 |
+
assert result[0].text == "1" # type: ignore[attr-defined]
|
|
|
|
| 446 |
|
| 447 |
result = await client.call_tool("analyze", {"x": 1.0})
|
| 448 |
+
assert result[0].text == "1.0" # type: ignore[attr-defined]
|
|
|
|
| 449 |
|
| 450 |
with pytest.raises(ToolError, match="Error calling tool 'analyze'"):
|
| 451 |
await client.call_tool("analyze", {"x": "not a number"})
|
|
|
|
| 463 |
|
| 464 |
async with Client(mcp) as client:
|
| 465 |
result = await client.call_tool("send_path", {"path": str(test_path)})
|
| 466 |
+
assert result[0].text == str(test_path) # type: ignore[attr-defined]
|
|
|
|
| 467 |
|
| 468 |
async def test_path_type_error(self):
|
| 469 |
mcp = FastMCP()
|
|
|
|
| 488 |
|
| 489 |
async with Client(mcp) as client:
|
| 490 |
result = await client.call_tool("send_uuid", {"x": test_uuid})
|
| 491 |
+
assert result[0].text == str(test_uuid) # type: ignore[attr-defined]
|
|
|
|
| 492 |
|
| 493 |
async def test_uuid_type_error(self):
|
| 494 |
mcp = FastMCP()
|
|
|
|
| 512 |
|
| 513 |
async with Client(mcp) as client:
|
| 514 |
result = await client.call_tool("send_datetime", {"x": dt})
|
| 515 |
+
assert result[0].text == dt.isoformat() # type: ignore[attr-defined]
|
|
|
|
| 516 |
|
| 517 |
async def test_datetime_type_parse_string(self):
|
| 518 |
mcp = FastMCP()
|
|
|
|
| 525 |
result = await client.call_tool(
|
| 526 |
"send_datetime", {"x": "2021-01-01T00:00:00"}
|
| 527 |
)
|
| 528 |
+
assert result[0].text == "2021-01-01T00:00:00" # type: ignore[attr-defined]
|
|
|
|
| 529 |
|
| 530 |
async def test_datetime_type_error(self):
|
| 531 |
mcp = FastMCP()
|
|
|
|
| 547 |
|
| 548 |
async with Client(mcp) as client:
|
| 549 |
result = await client.call_tool("send_date", {"x": datetime.date.today()})
|
| 550 |
+
assert result[0].text == datetime.date.today().isoformat() # type: ignore[attr-defined]
|
|
|
|
| 551 |
|
| 552 |
async def test_date_type_parse_string(self):
|
| 553 |
mcp = FastMCP()
|
|
|
|
| 558 |
|
| 559 |
async with Client(mcp) as client:
|
| 560 |
result = await client.call_tool("send_date", {"x": "2021-01-01"})
|
| 561 |
+
assert result[0].text == "2021-01-01" # type: ignore[attr-defined]
|
|
|
|
| 562 |
|
| 563 |
async def test_timedelta_type(self):
|
| 564 |
mcp = FastMCP()
|
|
|
|
| 571 |
result = await client.call_tool(
|
| 572 |
"send_timedelta", {"x": datetime.timedelta(days=1)}
|
| 573 |
)
|
| 574 |
+
assert result[0].text == "1 day, 0:00:00" # type: ignore[attr-defined]
|
|
|
|
| 575 |
|
| 576 |
async def test_timedelta_type_parse_int(self):
|
| 577 |
mcp = FastMCP()
|
|
|
|
| 582 |
|
| 583 |
async with Client(mcp) as client:
|
| 584 |
result = await client.call_tool("send_timedelta", {"x": 1000})
|
| 585 |
+
assert result[0].text == "0:16:40" # type: ignore[attr-defined]
|
|
|
|
| 586 |
|
| 587 |
|
| 588 |
class TestToolContextInjection:
|
|
|
|
| 615 |
result = await client.call_tool("tool_with_context", {"x": 42})
|
| 616 |
assert len(result) == 1
|
| 617 |
content = result[0]
|
| 618 |
+
assert content.text == "2" # type: ignore[attr-defined]
|
| 619 |
|
| 620 |
async def test_async_context(self):
|
| 621 |
"""Test that context works in async functions."""
|
|
|
|
| 630 |
result = await client.call_tool("async_tool", {"x": 42})
|
| 631 |
assert len(result) == 1
|
| 632 |
content = result[0]
|
| 633 |
+
assert content.text == "Async request 2: 42" # type: ignore[attr-defined]
|
|
|
|
| 634 |
|
| 635 |
async def test_optional_context(self):
|
| 636 |
"""Test that context is optional."""
|
|
|
|
| 644 |
result = await client.call_tool("no_context", {"x": 21})
|
| 645 |
assert len(result) == 1
|
| 646 |
content = result[0]
|
| 647 |
+
assert content.text == "42" # type: ignore[attr-defined]
|
|
|
|
| 648 |
|
| 649 |
async def test_context_resource_access(self):
|
| 650 |
"""Test that context can access resources."""
|
|
|
|
| 666 |
result = await client.call_tool("tool_with_resource", {})
|
| 667 |
assert len(result) == 1
|
| 668 |
content = result[0]
|
| 669 |
+
assert "Read resource: resource data" in content.text # type: ignore[attr-defined]
|
|
|
|
| 670 |
|
| 671 |
async def test_tool_decorator_with_tags(self):
|
| 672 |
"""Test that the tool decorator properly sets tags."""
|
|
|
|
| 694 |
|
| 695 |
async with Client(mcp) as client:
|
| 696 |
result = await client.call_tool("MyTool", {"x": 2})
|
| 697 |
+
assert result[0].text == "4" # type: ignore[attr-defined]
|
|
|
|
| 698 |
|
| 699 |
|
| 700 |
class TestResource:
|
|
|
|
| 711 |
|
| 712 |
async with Client(mcp) as client:
|
| 713 |
result = await client.read_resource(AnyUrl("resource://test"))
|
| 714 |
+
assert result[0].text == "Hello, world!" # type: ignore[attr-defined]
|
|
|
|
| 715 |
|
| 716 |
async def test_binary_resource(self):
|
| 717 |
mcp = FastMCP()
|
|
|
|
| 729 |
|
| 730 |
async with Client(mcp) as client:
|
| 731 |
result = await client.read_resource(AnyUrl("resource://binary"))
|
| 732 |
+
assert result[0].blob == base64.b64encode(b"Binary data").decode() # type: ignore[attr-defined]
|
|
|
|
| 733 |
|
| 734 |
async def test_file_resource_text(self, tmp_path: Path):
|
| 735 |
mcp = FastMCP()
|
|
|
|
| 745 |
|
| 746 |
async with Client(mcp) as client:
|
| 747 |
result = await client.read_resource(AnyUrl("file://test.txt"))
|
| 748 |
+
assert result[0].text == "Hello from file!" # type: ignore[attr-defined]
|
|
|
|
| 749 |
|
| 750 |
async def test_file_resource_binary(self, tmp_path: Path):
|
| 751 |
mcp = FastMCP()
|
|
|
|
| 764 |
|
| 765 |
async with Client(mcp) as client:
|
| 766 |
result = await client.read_resource(AnyUrl("file://test.bin"))
|
| 767 |
+
assert result[0].blob == base64.b64encode(b"Binary file data").decode() # type: ignore[attr-defined]
|
|
|
|
| 768 |
|
| 769 |
|
| 770 |
class TestResourceContext:
|
|
|
|
| 778 |
|
| 779 |
async with Client(mcp) as client:
|
| 780 |
result = await client.read_resource(AnyUrl("resource://test"))
|
| 781 |
+
assert result[0].text == "2" # type: ignore[attr-defined]
|
|
|
|
| 782 |
|
| 783 |
|
| 784 |
class TestResourceTemplates:
|
|
|
|
| 827 |
|
| 828 |
async with Client(mcp) as client:
|
| 829 |
result = await client.read_resource(AnyUrl("resource://test/data"))
|
| 830 |
+
assert result[0].text == "Data for test" # type: ignore[attr-defined]
|
|
|
|
| 831 |
|
| 832 |
async def test_resource_mismatched_params(self):
|
| 833 |
"""Test that mismatched parameters raise an error"""
|
|
|
|
| 854 |
result = await client.read_resource(
|
| 855 |
AnyUrl("resource://cursor/fastmcp/data")
|
| 856 |
)
|
| 857 |
+
assert result[0].text == "Data for cursor/fastmcp" # type: ignore[attr-defined]
|
|
|
|
| 858 |
|
| 859 |
async def test_resource_multiple_mismatched_params(self):
|
| 860 |
"""Test that mismatched parameters raise an error"""
|
|
|
|
| 878 |
|
| 879 |
async with Client(mcp) as client:
|
| 880 |
result = await client.read_resource(AnyUrl("resource://static"))
|
| 881 |
+
assert result[0].text == "Static data" # type: ignore[attr-defined]
|
|
|
|
| 882 |
|
| 883 |
async def test_template_with_varkwargs(self):
|
| 884 |
"""Test that a template can have **kwargs."""
|
|
|
|
| 890 |
|
| 891 |
async with Client(mcp) as client:
|
| 892 |
result = await client.read_resource(AnyUrl("test://1/2/3"))
|
| 893 |
+
assert result[0].text == "6" # type: ignore[attr-defined]
|
|
|
|
| 894 |
|
| 895 |
async def test_template_with_default_params(self):
|
| 896 |
"""Test that a template can have default parameters."""
|
|
|
|
| 909 |
# Call the template and verify it uses the default value
|
| 910 |
async with Client(mcp) as client:
|
| 911 |
result = await client.read_resource(AnyUrl("math://add/5"))
|
| 912 |
+
assert result[0].text == "15" # type: ignore[attr-defined]
|
|
|
|
| 913 |
|
| 914 |
# Can also call with explicit params
|
| 915 |
result2 = await client.read_resource(AnyUrl("math://add/7"))
|
| 916 |
+
assert result2[0].text == "17" # type: ignore[attr-defined]
|
|
|
|
| 917 |
|
| 918 |
async def test_template_to_resource_conversion(self):
|
| 919 |
"""Test that a template can be converted to a resource."""
|
|
|
|
| 932 |
# When accessed, should create a concrete resource
|
| 933 |
async with Client(mcp) as client:
|
| 934 |
result = await client.read_resource(AnyUrl("resource://test/data"))
|
| 935 |
+
assert result[0].text == "Data for test" # type: ignore[attr-defined]
|
|
|
|
| 936 |
|
| 937 |
async def test_stacked_resource_template_decorators(self):
|
| 938 |
"""Test that resource template decorators can be stacked."""
|
|
|
|
| 971 |
email_result = await client.read_resource(
|
| 972 |
AnyUrl("users://email/user@example.com")
|
| 973 |
)
|
| 974 |
+
assert email_result[0].text # type: ignore[attr-defined]
|
| 975 |
+
email_data = json.loads(email_result[0].text) # type: ignore[attr-defined]
|
| 976 |
assert email_data["lookup"] == "email"
|
| 977 |
assert email_data["email"] == "user@example.com"
|
| 978 |
|
| 979 |
# Test lookup by name
|
| 980 |
name_result = await client.read_resource(AnyUrl("users://name/John"))
|
| 981 |
+
assert name_result[0].text # type: ignore[attr-defined]
|
| 982 |
+
name_
|
| 983 |
assert name_data["lookup"] == "name"
|
| 984 |
assert name_data["name"] == "John"
|
| 985 |
assert name_data["email"] == "dummy@example.com"
|
|
|
|
| 1004 |
|
| 1005 |
async with Client(mcp) as client:
|
| 1006 |
result = await client.read_resource(AnyUrl("resource://test/data"))
|
| 1007 |
+
assert result[0].text == "Template resource: test/data" # type: ignore[attr-defined]
|
|
|
|
| 1008 |
|
| 1009 |
async def test_templates_match_in_order_of_definition(self):
|
| 1010 |
"""
|
|
|
|
| 1024 |
|
| 1025 |
async with Client(mcp) as client:
|
| 1026 |
result = await client.read_resource(AnyUrl("resource://a/b/c"))
|
| 1027 |
+
assert result[0].text == "Template resource 1: a/b/c" # type: ignore[attr-defined]
|
|
|
|
| 1028 |
|
| 1029 |
result = await client.read_resource(AnyUrl("resource://a/b"))
|
| 1030 |
+
assert result[0].text == "Template resource 1: a/b" # type: ignore[attr-defined]
|
|
|
|
| 1031 |
|
| 1032 |
async def test_templates_shadow_each_other_reorder(self):
|
| 1033 |
"""
|
|
|
|
| 1046 |
|
| 1047 |
async with Client(mcp) as client:
|
| 1048 |
result = await client.read_resource(AnyUrl("resource://a/b/c"))
|
| 1049 |
+
assert result[0].text == "Template resource 2: a/b/c" # type: ignore[attr-defined]
|
|
|
|
| 1050 |
|
| 1051 |
result = await client.read_resource(AnyUrl("resource://a/b"))
|
| 1052 |
+
assert result[0].text == "Template resource 1: a/b" # type: ignore[attr-defined]
|
|
|
|
| 1053 |
|
| 1054 |
|
| 1055 |
class TestResourceTemplateContext:
|
|
|
|
| 1063 |
|
| 1064 |
async with Client(mcp) as client:
|
| 1065 |
result = await client.read_resource(AnyUrl("resource://test"))
|
| 1066 |
+
assert result[0].text.startswith("Resource template: test 2") # type: ignore[attr-defined]
|
|
|
|
| 1067 |
|
| 1068 |
async def test_resource_template_context_with_callable_object(self):
|
| 1069 |
mcp = FastMCP()
|
|
|
|
| 1076 |
|
| 1077 |
async with Client(mcp) as client:
|
| 1078 |
result = await client.read_resource(AnyUrl("resource://test"))
|
| 1079 |
+
assert result[0].text.startswith("Resource template: test 2") # type: ignore[attr-defined]
|
|
|
|
| 1080 |
|
| 1081 |
|
| 1082 |
class TestPrompts:
|
|
|
|
| 1096 |
assert prompt.name == "fn"
|
| 1097 |
# Don't compare functions directly since validate_call wraps them
|
| 1098 |
content = await prompt.render()
|
| 1099 |
+
assert content[0].content.text == "Hello, world!" # type: ignore[attr-defined]
|
|
|
|
| 1100 |
|
| 1101 |
async def test_prompt_decorator_with_name(self):
|
| 1102 |
"""Test prompt decorator with custom name."""
|
|
|
|
| 1111 |
prompt = prompts_dict["custom_name"]
|
| 1112 |
assert prompt.name == "custom_name"
|
| 1113 |
content = await prompt.render()
|
| 1114 |
+
assert content[0].content.text == "Hello, world!" # type: ignore[attr-defined]
|
|
|
|
| 1115 |
|
| 1116 |
async def test_prompt_decorator_with_description(self):
|
| 1117 |
"""Test prompt decorator with custom description."""
|
|
|
|
| 1126 |
prompt = prompts_dict["fn"]
|
| 1127 |
assert prompt.description == "A custom description"
|
| 1128 |
content = await prompt.render()
|
| 1129 |
+
assert content[0].content.text == "Hello, world!" # type: ignore[attr-defined]
|
|
|
|
| 1130 |
|
| 1131 |
def test_prompt_decorator_error(self):
|
| 1132 |
"""Test error when decorator is used incorrectly."""
|
|
|
|
| 1174 |
message = result.messages[0]
|
| 1175 |
assert message.role == "user"
|
| 1176 |
content = message.content
|
| 1177 |
+
assert content.text == "Hello, World!" # type: ignore[attr-defined]
|
|
|
|
| 1178 |
|
| 1179 |
async def test_get_prompt_with_resource(self):
|
| 1180 |
"""Test getting a prompt that returns resource content."""
|
|
|
|
| 1198 |
result = await client.get_prompt("fn")
|
| 1199 |
assert result.messages[0].role == "user"
|
| 1200 |
content = result.messages[0].content
|
| 1201 |
+
assert isinstance(content, EmbeddedResource) # type: ignore[attr-defined]
|
| 1202 |
resource = content.resource
|
| 1203 |
+
assert isinstance(resource, TextResourceContents) # type: ignore[attr-defined]
|
| 1204 |
+
assert resource.text == "File contents" # type: ignore[attr-defined]
|
| 1205 |
assert resource.mimeType == "text/plain"
|
| 1206 |
|
| 1207 |
async def test_get_unknown_prompt(self):
|
|
|
|
| 1291 |
assert len(result.messages) == 1
|
| 1292 |
message = result.messages[0]
|
| 1293 |
assert message.role == "user"
|
| 1294 |
+
assert message.content.text == "Hello, World! 2" # type: ignore[attr-defined]
|
|
|
tests/server/test_tool_annotations.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
from typing import Any
|
| 2 |
|
| 3 |
-
from mcp.types import
|
| 4 |
|
| 5 |
from fastmcp import Client, FastMCP
|
| 6 |
|
|
@@ -212,8 +212,7 @@ async def test_tool_functionality_with_annotations():
|
|
| 212 |
"create_item", {"name": "test_item", "value": 42}
|
| 213 |
)
|
| 214 |
assert len(result) == 1
|
| 215 |
-
assert isinstance(result[0], TextContent)
|
| 216 |
|
| 217 |
# The result should contain the expected JSON
|
| 218 |
-
assert '"name": "test_item"' in result[0].text
|
| 219 |
-
assert '"value": 42' in result[0].text
|
|
|
|
| 1 |
from typing import Any
|
| 2 |
|
| 3 |
+
from mcp.types import ToolAnnotations
|
| 4 |
|
| 5 |
from fastmcp import Client, FastMCP
|
| 6 |
|
|
|
|
| 212 |
"create_item", {"name": "test_item", "value": 42}
|
| 213 |
)
|
| 214 |
assert len(result) == 1
|
|
|
|
| 215 |
|
| 216 |
# The result should contain the expected JSON
|
| 217 |
+
assert '"name": "test_item"' in result[0].text # type: ignore[attr-defined]
|
| 218 |
+
assert '"value": 42' in result[0].text # type: ignore[attr-defined]
|
tests/test_examples.py
CHANGED
|
@@ -1,10 +1,5 @@
|
|
| 1 |
"""Tests for example servers"""
|
| 2 |
|
| 3 |
-
from mcp.types import (
|
| 4 |
-
PromptMessage,
|
| 5 |
-
TextContent,
|
| 6 |
-
TextResourceContents,
|
| 7 |
-
)
|
| 8 |
from pydantic import AnyUrl
|
| 9 |
|
| 10 |
from fastmcp import Client
|
|
@@ -17,8 +12,7 @@ async def test_simple_echo():
|
|
| 17 |
async with Client(mcp) as client:
|
| 18 |
result = await client.call_tool("echo", {"text": "hello"})
|
| 19 |
assert len(result) == 1
|
| 20 |
-
assert
|
| 21 |
-
assert result[0].text == "hello"
|
| 22 |
|
| 23 |
|
| 24 |
async def test_complex_inputs():
|
|
@@ -31,8 +25,7 @@ async def test_complex_inputs():
|
|
| 31 |
"name_shrimp", {"tank": tank, "extra_names": ["charlie"]}
|
| 32 |
)
|
| 33 |
assert len(result) == 1
|
| 34 |
-
assert
|
| 35 |
-
assert result[0].text == '[\n "bob",\n "alice",\n "charlie"\n]'
|
| 36 |
|
| 37 |
|
| 38 |
async def test_desktop(monkeypatch):
|
|
@@ -43,15 +36,12 @@ async def test_desktop(monkeypatch):
|
|
| 43 |
# Test the add function
|
| 44 |
result = await client.call_tool("add", {"a": 1, "b": 2})
|
| 45 |
assert len(result) == 1
|
| 46 |
-
assert
|
| 47 |
-
assert result[0].text == "3"
|
| 48 |
|
| 49 |
async with Client(mcp) as client:
|
| 50 |
result = await client.read_resource(AnyUrl("greeting://rooter12"))
|
| 51 |
assert len(result) == 1
|
| 52 |
-
assert
|
| 53 |
-
assert isinstance(result[0].text, str)
|
| 54 |
-
assert result[0].text == "Hello, rooter12!"
|
| 55 |
|
| 56 |
|
| 57 |
async def test_echo():
|
|
@@ -61,27 +51,19 @@ async def test_echo():
|
|
| 61 |
async with Client(mcp) as client:
|
| 62 |
result = await client.call_tool("echo_tool", {"text": "hello"})
|
| 63 |
assert len(result) == 1
|
| 64 |
-
assert
|
| 65 |
-
assert result[0].text == "hello"
|
| 66 |
|
| 67 |
async with Client(mcp) as client:
|
| 68 |
result = await client.read_resource(AnyUrl("echo://static"))
|
| 69 |
assert len(result) == 1
|
| 70 |
-
assert
|
| 71 |
-
assert isinstance(result[0].text, str)
|
| 72 |
-
assert result[0].text == "Echo!"
|
| 73 |
|
| 74 |
async with Client(mcp) as client:
|
| 75 |
result = await client.read_resource(AnyUrl("echo://server42"))
|
| 76 |
assert len(result) == 1
|
| 77 |
-
assert
|
| 78 |
-
assert isinstance(result[0].text, str)
|
| 79 |
-
assert result[0].text == "Echo: server42"
|
| 80 |
|
| 81 |
async with Client(mcp) as client:
|
| 82 |
result = await client.get_prompt("echo", {"text": "hello"})
|
| 83 |
assert len(result.messages) == 1
|
| 84 |
-
assert
|
| 85 |
-
assert isinstance(result.messages[0].content, TextContent)
|
| 86 |
-
assert isinstance(result.messages[0].content.text, str)
|
| 87 |
-
assert result.messages[0].content.text == "hello"
|
|
|
|
| 1 |
"""Tests for example servers"""
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
from pydantic import AnyUrl
|
| 4 |
|
| 5 |
from fastmcp import Client
|
|
|
|
| 12 |
async with Client(mcp) as client:
|
| 13 |
result = await client.call_tool("echo", {"text": "hello"})
|
| 14 |
assert len(result) == 1
|
| 15 |
+
assert result[0].text == "hello" # type: ignore[attr-defined]
|
|
|
|
| 16 |
|
| 17 |
|
| 18 |
async def test_complex_inputs():
|
|
|
|
| 25 |
"name_shrimp", {"tank": tank, "extra_names": ["charlie"]}
|
| 26 |
)
|
| 27 |
assert len(result) == 1
|
| 28 |
+
assert result[0].text == '[\n "bob",\n "alice",\n "charlie"\n]' # type: ignore[attr-defined]
|
|
|
|
| 29 |
|
| 30 |
|
| 31 |
async def test_desktop(monkeypatch):
|
|
|
|
| 36 |
# Test the add function
|
| 37 |
result = await client.call_tool("add", {"a": 1, "b": 2})
|
| 38 |
assert len(result) == 1
|
| 39 |
+
assert result[0].text == "3" # type: ignore[attr-defined]
|
|
|
|
| 40 |
|
| 41 |
async with Client(mcp) as client:
|
| 42 |
result = await client.read_resource(AnyUrl("greeting://rooter12"))
|
| 43 |
assert len(result) == 1
|
| 44 |
+
assert result[0].text == "Hello, rooter12!" # type: ignore[attr-defined]
|
|
|
|
|
|
|
| 45 |
|
| 46 |
|
| 47 |
async def test_echo():
|
|
|
|
| 51 |
async with Client(mcp) as client:
|
| 52 |
result = await client.call_tool("echo_tool", {"text": "hello"})
|
| 53 |
assert len(result) == 1
|
| 54 |
+
assert result[0].text == "hello" # type: ignore[attr-defined]
|
|
|
|
| 55 |
|
| 56 |
async with Client(mcp) as client:
|
| 57 |
result = await client.read_resource(AnyUrl("echo://static"))
|
| 58 |
assert len(result) == 1
|
| 59 |
+
assert result[0].text == "Echo!" # type: ignore[attr-defined]
|
|
|
|
|
|
|
| 60 |
|
| 61 |
async with Client(mcp) as client:
|
| 62 |
result = await client.read_resource(AnyUrl("echo://server42"))
|
| 63 |
assert len(result) == 1
|
| 64 |
+
assert result[0].text == "Echo: server42" # type: ignore[attr-defined]
|
|
|
|
|
|
|
| 65 |
|
| 66 |
async with Client(mcp) as client:
|
| 67 |
result = await client.get_prompt("echo", {"text": "hello"})
|
| 68 |
assert len(result.messages) == 1
|
| 69 |
+
assert result.messages[0].content.text == "hello" # type: ignore[attr-defined]
|
|
|
|
|
|
|
|
|
tests/tools/test_tool.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import pytest
|
| 2 |
-
from mcp.types import ImageContent
|
| 3 |
from pydantic import BaseModel
|
| 4 |
|
| 5 |
from fastmcp import FastMCP, Image
|
|
@@ -209,9 +209,7 @@ class TestLegacyToolJsonParsing:
|
|
| 209 |
|
| 210 |
# Run the tool which will do JSON parsing
|
| 211 |
result = await tool.run(json_args)
|
| 212 |
-
assert
|
| 213 |
-
assert isinstance(result[0], TextContent)
|
| 214 |
-
assert result[0].text == "1-a,b,c"
|
| 215 |
|
| 216 |
async def test_str_vs_list_str(self):
|
| 217 |
"""Test handling of string vs list[str] type annotations."""
|
|
@@ -223,23 +221,17 @@ class TestLegacyToolJsonParsing:
|
|
| 223 |
|
| 224 |
# Test regular string input (should remain a string)
|
| 225 |
result = await tool.run({"str_or_list": "hello"})
|
| 226 |
-
assert
|
| 227 |
-
assert isinstance(result[0], TextContent)
|
| 228 |
-
assert result[0].text == "hello"
|
| 229 |
|
| 230 |
# Test JSON string input (should be parsed as a string)
|
| 231 |
result = await tool.run({"str_or_list": '"hello"'})
|
| 232 |
-
assert
|
| 233 |
-
assert isinstance(result[0], TextContent)
|
| 234 |
-
assert result[0].text == "hello"
|
| 235 |
|
| 236 |
# Test JSON list input (should be parsed as a list)
|
| 237 |
result = await tool.run({"str_or_list": '["hello", "world"]'})
|
| 238 |
-
assert len(result) == 1
|
| 239 |
-
assert isinstance(result[0], TextContent)
|
| 240 |
|
| 241 |
# The exact formatting might vary, so we just check that it contains the key elements
|
| 242 |
-
text_without_whitespace = result[0].text.replace(" ", "").replace("\n", "")
|
| 243 |
assert "hello" in text_without_whitespace
|
| 244 |
assert "world" in text_without_whitespace
|
| 245 |
assert "[" in text_without_whitespace
|
|
@@ -256,9 +248,7 @@ class TestLegacyToolJsonParsing:
|
|
| 256 |
# Invalid JSON should remain a string
|
| 257 |
invalid_json = "{'nice to meet you': 'hello', 'goodbye': 5}"
|
| 258 |
result = await tool.run({"string": invalid_json})
|
| 259 |
-
assert
|
| 260 |
-
assert isinstance(result[0], TextContent)
|
| 261 |
-
assert result[0].text == invalid_json
|
| 262 |
|
| 263 |
async def test_keep_str_union_as_str(self):
|
| 264 |
"""Test that string arguments are kept as strings when parsing would create an invalid value"""
|
|
@@ -273,9 +263,7 @@ class TestLegacyToolJsonParsing:
|
|
| 273 |
# Invalid JSON for the union type should remain a string
|
| 274 |
invalid_json = "{'nice to meet you': 'hello', 'goodbye': 5}"
|
| 275 |
result = await tool.run({"string": invalid_json})
|
| 276 |
-
assert
|
| 277 |
-
assert isinstance(result[0], TextContent)
|
| 278 |
-
assert result[0].text == invalid_json
|
| 279 |
|
| 280 |
async def test_complex_type_validation(self):
|
| 281 |
"""Test that parsed JSON is validated against complex types"""
|
|
@@ -292,11 +280,9 @@ class TestLegacyToolJsonParsing:
|
|
| 292 |
# Valid JSON for the model
|
| 293 |
valid_json = '{"x": 1, "y": {"1": "hello"}}'
|
| 294 |
result = await tool.run({"data": valid_json})
|
| 295 |
-
assert
|
| 296 |
-
assert
|
| 297 |
-
assert '"
|
| 298 |
-
assert '"y": {' in result[0].text
|
| 299 |
-
assert '"1": "hello"' in result[0].text
|
| 300 |
|
| 301 |
# Invalid JSON for the model (y has string keys, not int keys)
|
| 302 |
# Should throw a validation error
|
|
@@ -317,8 +303,7 @@ class TestLegacyToolJsonParsing:
|
|
| 317 |
result = await client.call_tool(
|
| 318 |
"process_list", {"items": "[1, 2, 3, 4, 5]"}
|
| 319 |
)
|
| 320 |
-
assert
|
| 321 |
-
assert result[0].text == "15"
|
| 322 |
|
| 323 |
async def test_tool_list_coercion_error(self):
|
| 324 |
"""Test that a list coercion error is raised if the input is not a valid list."""
|
|
@@ -348,8 +333,7 @@ class TestLegacyToolJsonParsing:
|
|
| 348 |
result = await client.call_tool(
|
| 349 |
"process_dict", {"data": '{"a": 1, "b": "2", "c": 3}'}
|
| 350 |
)
|
| 351 |
-
assert
|
| 352 |
-
assert result[0].text == "6"
|
| 353 |
|
| 354 |
async def test_tool_set_coercion(self):
|
| 355 |
"""Test JSON string to set type coercion."""
|
|
@@ -362,8 +346,7 @@ class TestLegacyToolJsonParsing:
|
|
| 362 |
|
| 363 |
async with Client(mcp) as client:
|
| 364 |
result = await client.call_tool("process_set", {"items": "[1, 2, 3, 4, 5]"})
|
| 365 |
-
assert
|
| 366 |
-
assert result[0].text == "15"
|
| 367 |
|
| 368 |
async def test_tool_tuple_coercion(self):
|
| 369 |
"""Test JSON string to tuple type coercion."""
|
|
@@ -376,5 +359,4 @@ class TestLegacyToolJsonParsing:
|
|
| 376 |
|
| 377 |
async with Client(mcp) as client:
|
| 378 |
result = await client.call_tool("process_tuple", {"items": '["1", "two"]'})
|
| 379 |
-
assert
|
| 380 |
-
assert result[0].text == "4"
|
|
|
|
| 1 |
import pytest
|
| 2 |
+
from mcp.types import ImageContent
|
| 3 |
from pydantic import BaseModel
|
| 4 |
|
| 5 |
from fastmcp import FastMCP, Image
|
|
|
|
| 209 |
|
| 210 |
# Run the tool which will do JSON parsing
|
| 211 |
result = await tool.run(json_args)
|
| 212 |
+
assert result[0].text == "1-a,b,c" # type: ignore[attr-dict]
|
|
|
|
|
|
|
| 213 |
|
| 214 |
async def test_str_vs_list_str(self):
|
| 215 |
"""Test handling of string vs list[str] type annotations."""
|
|
|
|
| 221 |
|
| 222 |
# Test regular string input (should remain a string)
|
| 223 |
result = await tool.run({"str_or_list": "hello"})
|
| 224 |
+
assert result[0].text == "hello" # type: ignore[attr-dict]
|
|
|
|
|
|
|
| 225 |
|
| 226 |
# Test JSON string input (should be parsed as a string)
|
| 227 |
result = await tool.run({"str_or_list": '"hello"'})
|
| 228 |
+
assert result[0].text == "hello" # type: ignore[attr-dict]
|
|
|
|
|
|
|
| 229 |
|
| 230 |
# Test JSON list input (should be parsed as a list)
|
| 231 |
result = await tool.run({"str_or_list": '["hello", "world"]'})
|
|
|
|
|
|
|
| 232 |
|
| 233 |
# The exact formatting might vary, so we just check that it contains the key elements
|
| 234 |
+
text_without_whitespace = result[0].text.replace(" ", "").replace("\n", "") # type: ignore[attr-dict]
|
| 235 |
assert "hello" in text_without_whitespace
|
| 236 |
assert "world" in text_without_whitespace
|
| 237 |
assert "[" in text_without_whitespace
|
|
|
|
| 248 |
# Invalid JSON should remain a string
|
| 249 |
invalid_json = "{'nice to meet you': 'hello', 'goodbye': 5}"
|
| 250 |
result = await tool.run({"string": invalid_json})
|
| 251 |
+
assert result[0].text == invalid_json # type: ignore[attr-dict]
|
|
|
|
|
|
|
| 252 |
|
| 253 |
async def test_keep_str_union_as_str(self):
|
| 254 |
"""Test that string arguments are kept as strings when parsing would create an invalid value"""
|
|
|
|
| 263 |
# Invalid JSON for the union type should remain a string
|
| 264 |
invalid_json = "{'nice to meet you': 'hello', 'goodbye': 5}"
|
| 265 |
result = await tool.run({"string": invalid_json})
|
| 266 |
+
assert result[0].text == invalid_json # type: ignore[attr-dict]
|
|
|
|
|
|
|
| 267 |
|
| 268 |
async def test_complex_type_validation(self):
|
| 269 |
"""Test that parsed JSON is validated against complex types"""
|
|
|
|
| 280 |
# Valid JSON for the model
|
| 281 |
valid_json = '{"x": 1, "y": {"1": "hello"}}'
|
| 282 |
result = await tool.run({"data": valid_json})
|
| 283 |
+
assert '"x": 1' in result[0].text # type: ignore[attr-dict]
|
| 284 |
+
assert '"y": {' in result[0].text # type: ignore[attr-dict]
|
| 285 |
+
assert '"1": "hello"' in result[0].text # type: ignore[attr-dict]
|
|
|
|
|
|
|
| 286 |
|
| 287 |
# Invalid JSON for the model (y has string keys, not int keys)
|
| 288 |
# Should throw a validation error
|
|
|
|
| 303 |
result = await client.call_tool(
|
| 304 |
"process_list", {"items": "[1, 2, 3, 4, 5]"}
|
| 305 |
)
|
| 306 |
+
assert result[0].text == "15" # type: ignore[attr-dict]
|
|
|
|
| 307 |
|
| 308 |
async def test_tool_list_coercion_error(self):
|
| 309 |
"""Test that a list coercion error is raised if the input is not a valid list."""
|
|
|
|
| 333 |
result = await client.call_tool(
|
| 334 |
"process_dict", {"data": '{"a": 1, "b": "2", "c": 3}'}
|
| 335 |
)
|
| 336 |
+
assert result[0].text == "6" # type: ignore[attr-dict]
|
|
|
|
| 337 |
|
| 338 |
async def test_tool_set_coercion(self):
|
| 339 |
"""Test JSON string to set type coercion."""
|
|
|
|
| 346 |
|
| 347 |
async with Client(mcp) as client:
|
| 348 |
result = await client.call_tool("process_set", {"items": "[1, 2, 3, 4, 5]"})
|
| 349 |
+
assert result[0].text == "15" # type: ignore[attr-dict]
|
|
|
|
| 350 |
|
| 351 |
async def test_tool_tuple_coercion(self):
|
| 352 |
"""Test JSON string to tuple type coercion."""
|
|
|
|
| 359 |
|
| 360 |
async with Client(mcp) as client:
|
| 361 |
result = await client.call_tool("process_tuple", {"items": '["1", "two"]'})
|
| 362 |
+
assert result[0].text == "4" # type: ignore[attr-dict]
|
|
|
tests/tools/test_tool_manager.py
CHANGED
|
@@ -5,7 +5,7 @@ from typing import Annotated, Any
|
|
| 5 |
|
| 6 |
import pydantic_core
|
| 7 |
import pytest
|
| 8 |
-
from mcp.types import ImageContent
|
| 9 |
from pydantic import BaseModel
|
| 10 |
|
| 11 |
from fastmcp import Context, FastMCP, Image
|
|
@@ -318,13 +318,8 @@ class TestCallTools:
|
|
| 318 |
manager = ToolManager()
|
| 319 |
manager.add_tool_from_fn(add)
|
| 320 |
result = await manager.call_tool("add", {"a": 1, "b": 2})
|
| 321 |
-
assert isinstance(result, list)
|
| 322 |
-
assert len(result) == 1
|
| 323 |
-
from mcp.types import TextContent
|
| 324 |
|
| 325 |
-
assert
|
| 326 |
-
assert result[0].text == "3"
|
| 327 |
-
assert json.loads(result[0].text) == 3
|
| 328 |
|
| 329 |
async def test_call_async_tool(self):
|
| 330 |
async def double(n: int) -> int:
|
|
@@ -334,12 +329,7 @@ class TestCallTools:
|
|
| 334 |
manager = ToolManager()
|
| 335 |
manager.add_tool_from_fn(double)
|
| 336 |
result = await manager.call_tool("double", {"n": 5})
|
| 337 |
-
assert
|
| 338 |
-
assert len(result) == 1
|
| 339 |
-
|
| 340 |
-
assert isinstance(result[0], TextContent)
|
| 341 |
-
assert result[0].text == "10"
|
| 342 |
-
assert json.loads(result[0].text) == 10
|
| 343 |
|
| 344 |
async def test_call_tool_callable_object(self):
|
| 345 |
class Adder:
|
|
@@ -352,11 +342,7 @@ class TestCallTools:
|
|
| 352 |
manager = ToolManager()
|
| 353 |
manager.add_tool_from_fn(Adder())
|
| 354 |
result = await manager.call_tool("Adder", {"x": 1, "y": 2})
|
| 355 |
-
assert
|
| 356 |
-
assert len(result) == 1
|
| 357 |
-
assert isinstance(result[0], TextContent)
|
| 358 |
-
assert result[0].text == "3"
|
| 359 |
-
assert json.loads(result[0].text) == 3
|
| 360 |
|
| 361 |
async def test_call_tool_callable_object_async(self):
|
| 362 |
class Adder:
|
|
@@ -369,11 +355,7 @@ class TestCallTools:
|
|
| 369 |
manager = ToolManager()
|
| 370 |
manager.add_tool_from_fn(Adder())
|
| 371 |
result = await manager.call_tool("Adder", {"x": 1, "y": 2})
|
| 372 |
-
assert
|
| 373 |
-
assert len(result) == 1
|
| 374 |
-
assert isinstance(result[0], TextContent)
|
| 375 |
-
assert result[0].text == "3"
|
| 376 |
-
assert json.loads(result[0].text) == 3
|
| 377 |
|
| 378 |
async def test_call_tool_with_default_args(self):
|
| 379 |
def add(a: int, b: int = 1) -> int:
|
|
@@ -383,12 +365,8 @@ class TestCallTools:
|
|
| 383 |
manager = ToolManager()
|
| 384 |
manager.add_tool_from_fn(add)
|
| 385 |
result = await manager.call_tool("add", {"a": 1})
|
| 386 |
-
assert isinstance(result, list)
|
| 387 |
-
assert len(result) == 1
|
| 388 |
|
| 389 |
-
assert
|
| 390 |
-
assert result[0].text == "2"
|
| 391 |
-
assert json.loads(result[0].text) == 2
|
| 392 |
|
| 393 |
async def test_call_tool_with_missing_args(self):
|
| 394 |
def add(a: int, b: int) -> int:
|
|
@@ -413,11 +391,7 @@ class TestCallTools:
|
|
| 413 |
manager.add_tool_from_fn(sum_vals)
|
| 414 |
|
| 415 |
result = await manager.call_tool("sum_vals", {"vals": [1, 2, 3]})
|
| 416 |
-
assert
|
| 417 |
-
assert len(result) == 1
|
| 418 |
-
assert isinstance(result[0], TextContent)
|
| 419 |
-
assert result[0].text == "6"
|
| 420 |
-
assert json.loads(result[0].text) == 6
|
| 421 |
|
| 422 |
async def test_call_tool_with_list_int_input_legacy_behavior(self):
|
| 423 |
"""Legacy behavior -- parse a stringified JSON object"""
|
|
@@ -431,11 +405,7 @@ class TestCallTools:
|
|
| 431 |
|
| 432 |
with temporary_settings(tool_attempt_parse_json_args=True):
|
| 433 |
result = await manager.call_tool("sum_vals", {"vals": "[1, 2, 3]"})
|
| 434 |
-
assert
|
| 435 |
-
assert len(result) == 1
|
| 436 |
-
assert isinstance(result[0], TextContent)
|
| 437 |
-
assert result[0].text == "6"
|
| 438 |
-
assert json.loads(result[0].text) == 6
|
| 439 |
|
| 440 |
async def test_call_tool_with_list_str_or_str_input(self):
|
| 441 |
def concat_strs(vals: list[str] | str) -> str:
|
|
@@ -446,16 +416,10 @@ class TestCallTools:
|
|
| 446 |
|
| 447 |
# Try both with plain python object and with JSON list
|
| 448 |
result = await manager.call_tool("concat_strs", {"vals": ["a", "b", "c"]})
|
| 449 |
-
assert
|
| 450 |
-
assert len(result) == 1
|
| 451 |
-
assert isinstance(result[0], TextContent)
|
| 452 |
-
assert result[0].text == "abc"
|
| 453 |
|
| 454 |
result = await manager.call_tool("concat_strs", {"vals": "a"})
|
| 455 |
-
assert
|
| 456 |
-
assert len(result) == 1
|
| 457 |
-
assert isinstance(result[0], TextContent)
|
| 458 |
-
assert result[0].text == "a"
|
| 459 |
|
| 460 |
async def test_call_tool_with_list_str_or_str_input_legacy_behavior(self):
|
| 461 |
"""Legacy behavior -- parse a stringified JSON object"""
|
|
@@ -468,16 +432,10 @@ class TestCallTools:
|
|
| 468 |
|
| 469 |
with temporary_settings(tool_attempt_parse_json_args=True):
|
| 470 |
result = await manager.call_tool("concat_strs", {"vals": '["a", "b", "c"]'})
|
| 471 |
-
assert
|
| 472 |
-
assert len(result) == 1
|
| 473 |
-
assert isinstance(result[0], TextContent)
|
| 474 |
-
assert result[0].text == "abc"
|
| 475 |
|
| 476 |
result = await manager.call_tool("concat_strs", {"vals": '"a"'})
|
| 477 |
-
assert
|
| 478 |
-
assert len(result) == 1
|
| 479 |
-
assert isinstance(result[0], TextContent)
|
| 480 |
-
assert result[0].text == "a"
|
| 481 |
|
| 482 |
async def test_call_tool_with_complex_model(self):
|
| 483 |
class MyShrimpTank(BaseModel):
|
|
@@ -507,10 +465,7 @@ class TestCallTools:
|
|
| 507 |
},
|
| 508 |
)
|
| 509 |
|
| 510 |
-
assert
|
| 511 |
-
assert len(result) == 1
|
| 512 |
-
assert isinstance(result[0], TextContent)
|
| 513 |
-
assert result[0].text == '[\n "rex",\n "gertrude"\n]'
|
| 514 |
|
| 515 |
async def test_call_tool_with_custom_serializer(self):
|
| 516 |
"""Test that a custom serializer provided to FastMCP is used by tools."""
|
|
@@ -530,10 +485,7 @@ class TestCallTools:
|
|
| 530 |
manager.add_tool_from_fn(get_data)
|
| 531 |
|
| 532 |
result = await manager.call_tool("get_data", {})
|
| 533 |
-
assert
|
| 534 |
-
assert len(result) == 1
|
| 535 |
-
assert isinstance(result[0], TextContent)
|
| 536 |
-
assert result[0].text == 'CUSTOM:{"key": "value", "number": 123}'
|
| 537 |
|
| 538 |
async def test_call_tool_with_list_result_custom_serializer(self):
|
| 539 |
"""Test that a custom serializer provided to FastMCP is used by tools that return lists."""
|
|
@@ -555,12 +507,9 @@ class TestCallTools:
|
|
| 555 |
manager.add_tool_from_fn(get_data)
|
| 556 |
|
| 557 |
result = await manager.call_tool("get_data", {})
|
| 558 |
-
assert isinstance(result, list)
|
| 559 |
-
assert len(result) == 1
|
| 560 |
-
assert isinstance(result[0], TextContent)
|
| 561 |
assert (
|
| 562 |
-
result[0].text
|
| 563 |
-
== 'CUSTOM:[{"key": "value", "number": 123}, {"key": "value2", "number": 456}]'
|
| 564 |
)
|
| 565 |
|
| 566 |
async def test_custom_serializer_fallback_on_error(self):
|
|
@@ -580,10 +529,7 @@ class TestCallTools:
|
|
| 580 |
manager.add_tool_from_fn(get_data)
|
| 581 |
|
| 582 |
result = await manager.call_tool("get_data", {})
|
| 583 |
-
assert
|
| 584 |
-
assert len(result) == 1
|
| 585 |
-
assert isinstance(result[0], TextContent)
|
| 586 |
-
assert result[0].text == pydantic_core.to_json(uuid_result).decode()
|
| 587 |
|
| 588 |
|
| 589 |
class TestToolSchema:
|
|
@@ -648,10 +594,7 @@ class TestContextHandling:
|
|
| 648 |
|
| 649 |
with context:
|
| 650 |
result = await manager.call_tool("tool_with_context", {"x": 42})
|
| 651 |
-
assert
|
| 652 |
-
assert len(result) == 1
|
| 653 |
-
assert isinstance(result[0], TextContent)
|
| 654 |
-
assert result[0].text == "42"
|
| 655 |
|
| 656 |
async def test_context_injection_async(self):
|
| 657 |
"""Test that context is properly injected in async tools."""
|
|
@@ -668,14 +611,10 @@ class TestContextHandling:
|
|
| 668 |
|
| 669 |
with context:
|
| 670 |
result = await manager.call_tool("async_tool", {"x": 42})
|
| 671 |
-
assert
|
| 672 |
-
assert len(result) == 1
|
| 673 |
-
assert isinstance(result[0], TextContent)
|
| 674 |
-
assert result[0].text == "42"
|
| 675 |
|
| 676 |
async def test_context_optional(self):
|
| 677 |
"""Test that context is optional when calling tools."""
|
| 678 |
-
from mcp.types import TextContent
|
| 679 |
|
| 680 |
def tool_with_context(x: int, ctx: Context | None) -> int:
|
| 681 |
return x
|
|
@@ -689,10 +628,7 @@ class TestContextHandling:
|
|
| 689 |
|
| 690 |
with context:
|
| 691 |
result = await manager.call_tool("tool_with_context", {"x": 42})
|
| 692 |
-
assert
|
| 693 |
-
assert len(result) == 1
|
| 694 |
-
assert isinstance(result[0], TextContent)
|
| 695 |
-
assert result[0].text == "42"
|
| 696 |
|
| 697 |
def test_parameterized_context_parameter_detection(self):
|
| 698 |
"""Test that context parameters are properly detected in
|
|
@@ -782,7 +718,6 @@ class TestCustomToolNames:
|
|
| 782 |
|
| 783 |
async def test_call_tool_with_custom_name(self):
|
| 784 |
"""Test calling a tool added with a custom name."""
|
| 785 |
-
from mcp.types import TextContent
|
| 786 |
|
| 787 |
def multiply(a: int, b: int) -> int:
|
| 788 |
"""Multiply two numbers."""
|
|
@@ -793,11 +728,7 @@ class TestCustomToolNames:
|
|
| 793 |
|
| 794 |
# Tool should be callable by its custom name
|
| 795 |
result = await manager.call_tool("custom_multiply", {"a": 5, "b": 3})
|
| 796 |
-
assert
|
| 797 |
-
assert len(result) == 1
|
| 798 |
-
assert isinstance(result[0], TextContent)
|
| 799 |
-
assert result[0].text == "15"
|
| 800 |
-
assert json.loads(result[0].text) == 15
|
| 801 |
|
| 802 |
# Original name should not be registered
|
| 803 |
with pytest.raises(NotFoundError, match="Unknown tool: multiply"):
|
|
|
|
| 5 |
|
| 6 |
import pydantic_core
|
| 7 |
import pytest
|
| 8 |
+
from mcp.types import ImageContent
|
| 9 |
from pydantic import BaseModel
|
| 10 |
|
| 11 |
from fastmcp import Context, FastMCP, Image
|
|
|
|
| 318 |
manager = ToolManager()
|
| 319 |
manager.add_tool_from_fn(add)
|
| 320 |
result = await manager.call_tool("add", {"a": 1, "b": 2})
|
|
|
|
|
|
|
|
|
|
| 321 |
|
| 322 |
+
assert result[0].text == "3" # type: ignore[attr-defined]
|
|
|
|
|
|
|
| 323 |
|
| 324 |
async def test_call_async_tool(self):
|
| 325 |
async def double(n: int) -> int:
|
|
|
|
| 329 |
manager = ToolManager()
|
| 330 |
manager.add_tool_from_fn(double)
|
| 331 |
result = await manager.call_tool("double", {"n": 5})
|
| 332 |
+
assert result[0].text == "10" # type: ignore[attr-defined]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
|
| 334 |
async def test_call_tool_callable_object(self):
|
| 335 |
class Adder:
|
|
|
|
| 342 |
manager = ToolManager()
|
| 343 |
manager.add_tool_from_fn(Adder())
|
| 344 |
result = await manager.call_tool("Adder", {"x": 1, "y": 2})
|
| 345 |
+
assert result[0].text == "3" # type: ignore[attr-defined]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
|
| 347 |
async def test_call_tool_callable_object_async(self):
|
| 348 |
class Adder:
|
|
|
|
| 355 |
manager = ToolManager()
|
| 356 |
manager.add_tool_from_fn(Adder())
|
| 357 |
result = await manager.call_tool("Adder", {"x": 1, "y": 2})
|
| 358 |
+
assert result[0].text == "3" # type: ignore[attr-defined]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
|
| 360 |
async def test_call_tool_with_default_args(self):
|
| 361 |
def add(a: int, b: int = 1) -> int:
|
|
|
|
| 365 |
manager = ToolManager()
|
| 366 |
manager.add_tool_from_fn(add)
|
| 367 |
result = await manager.call_tool("add", {"a": 1})
|
|
|
|
|
|
|
| 368 |
|
| 369 |
+
assert result[0].text == "2" # type: ignore[attr-defined]
|
|
|
|
|
|
|
| 370 |
|
| 371 |
async def test_call_tool_with_missing_args(self):
|
| 372 |
def add(a: int, b: int) -> int:
|
|
|
|
| 391 |
manager.add_tool_from_fn(sum_vals)
|
| 392 |
|
| 393 |
result = await manager.call_tool("sum_vals", {"vals": [1, 2, 3]})
|
| 394 |
+
assert result[0].text == "6" # type: ignore[attr-defined]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
|
| 396 |
async def test_call_tool_with_list_int_input_legacy_behavior(self):
|
| 397 |
"""Legacy behavior -- parse a stringified JSON object"""
|
|
|
|
| 405 |
|
| 406 |
with temporary_settings(tool_attempt_parse_json_args=True):
|
| 407 |
result = await manager.call_tool("sum_vals", {"vals": "[1, 2, 3]"})
|
| 408 |
+
assert result[0].text == "6" # type: ignore[attr-defined]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
|
| 410 |
async def test_call_tool_with_list_str_or_str_input(self):
|
| 411 |
def concat_strs(vals: list[str] | str) -> str:
|
|
|
|
| 416 |
|
| 417 |
# Try both with plain python object and with JSON list
|
| 418 |
result = await manager.call_tool("concat_strs", {"vals": ["a", "b", "c"]})
|
| 419 |
+
assert result[0].text == "abc" # type: ignore[attr-defined]
|
|
|
|
|
|
|
|
|
|
| 420 |
|
| 421 |
result = await manager.call_tool("concat_strs", {"vals": "a"})
|
| 422 |
+
assert result[0].text == "a" # type: ignore[attr-defined]
|
|
|
|
|
|
|
|
|
|
| 423 |
|
| 424 |
async def test_call_tool_with_list_str_or_str_input_legacy_behavior(self):
|
| 425 |
"""Legacy behavior -- parse a stringified JSON object"""
|
|
|
|
| 432 |
|
| 433 |
with temporary_settings(tool_attempt_parse_json_args=True):
|
| 434 |
result = await manager.call_tool("concat_strs", {"vals": '["a", "b", "c"]'})
|
| 435 |
+
assert result[0].text == "abc" # type: ignore[attr-defined]
|
|
|
|
|
|
|
|
|
|
| 436 |
|
| 437 |
result = await manager.call_tool("concat_strs", {"vals": '"a"'})
|
| 438 |
+
assert result[0].text == "a" # type: ignore[attr-defined]
|
|
|
|
|
|
|
|
|
|
| 439 |
|
| 440 |
async def test_call_tool_with_complex_model(self):
|
| 441 |
class MyShrimpTank(BaseModel):
|
|
|
|
| 465 |
},
|
| 466 |
)
|
| 467 |
|
| 468 |
+
assert result[0].text == '[\n "rex",\n "gertrude"\n]' # type: ignore[attr-defined]
|
|
|
|
|
|
|
|
|
|
| 469 |
|
| 470 |
async def test_call_tool_with_custom_serializer(self):
|
| 471 |
"""Test that a custom serializer provided to FastMCP is used by tools."""
|
|
|
|
| 485 |
manager.add_tool_from_fn(get_data)
|
| 486 |
|
| 487 |
result = await manager.call_tool("get_data", {})
|
| 488 |
+
assert result[0].text == 'CUSTOM:{"key": "value", "number": 123}' # type: ignore[attr-defined]
|
|
|
|
|
|
|
|
|
|
| 489 |
|
| 490 |
async def test_call_tool_with_list_result_custom_serializer(self):
|
| 491 |
"""Test that a custom serializer provided to FastMCP is used by tools that return lists."""
|
|
|
|
| 507 |
manager.add_tool_from_fn(get_data)
|
| 508 |
|
| 509 |
result = await manager.call_tool("get_data", {})
|
|
|
|
|
|
|
|
|
|
| 510 |
assert (
|
| 511 |
+
result[0].text # type: ignore[attr-defined]
|
| 512 |
+
== 'CUSTOM:[{"key": "value", "number": 123}, {"key": "value2", "number": 456}]' # type: ignore[attr-defined]
|
| 513 |
)
|
| 514 |
|
| 515 |
async def test_custom_serializer_fallback_on_error(self):
|
|
|
|
| 529 |
manager.add_tool_from_fn(get_data)
|
| 530 |
|
| 531 |
result = await manager.call_tool("get_data", {})
|
| 532 |
+
assert result[0].text == pydantic_core.to_json(uuid_result).decode() # type: ignore[attr-defined]
|
|
|
|
|
|
|
|
|
|
| 533 |
|
| 534 |
|
| 535 |
class TestToolSchema:
|
|
|
|
| 594 |
|
| 595 |
with context:
|
| 596 |
result = await manager.call_tool("tool_with_context", {"x": 42})
|
| 597 |
+
assert result[0].text == "42" # type: ignore[attr-defined]
|
|
|
|
|
|
|
|
|
|
| 598 |
|
| 599 |
async def test_context_injection_async(self):
|
| 600 |
"""Test that context is properly injected in async tools."""
|
|
|
|
| 611 |
|
| 612 |
with context:
|
| 613 |
result = await manager.call_tool("async_tool", {"x": 42})
|
| 614 |
+
assert result[0].text == "42" # type: ignore[attr-defined]
|
|
|
|
|
|
|
|
|
|
| 615 |
|
| 616 |
async def test_context_optional(self):
|
| 617 |
"""Test that context is optional when calling tools."""
|
|
|
|
| 618 |
|
| 619 |
def tool_with_context(x: int, ctx: Context | None) -> int:
|
| 620 |
return x
|
|
|
|
| 628 |
|
| 629 |
with context:
|
| 630 |
result = await manager.call_tool("tool_with_context", {"x": 42})
|
| 631 |
+
assert result[0].text == "42" # type: ignore[attr-defined]
|
|
|
|
|
|
|
|
|
|
| 632 |
|
| 633 |
def test_parameterized_context_parameter_detection(self):
|
| 634 |
"""Test that context parameters are properly detected in
|
|
|
|
| 718 |
|
| 719 |
async def test_call_tool_with_custom_name(self):
|
| 720 |
"""Test calling a tool added with a custom name."""
|
|
|
|
| 721 |
|
| 722 |
def multiply(a: int, b: int) -> int:
|
| 723 |
"""Multiply two numbers."""
|
|
|
|
| 728 |
|
| 729 |
# Tool should be callable by its custom name
|
| 730 |
result = await manager.call_tool("custom_multiply", {"a": 5, "b": 3})
|
| 731 |
+
assert result[0].text == "15" # type: ignore[attr-defined]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 732 |
|
| 733 |
# Original name should not be registered
|
| 734 |
with pytest.raises(NotFoundError, match="Unknown tool: multiply"):
|
tests/utilities/test_mcp_config.py
CHANGED
|
@@ -1,8 +1,6 @@
|
|
| 1 |
import inspect
|
| 2 |
from pathlib import Path
|
| 3 |
|
| 4 |
-
from mcp.types import TextContent
|
| 5 |
-
|
| 6 |
from fastmcp.client.client import Client
|
| 7 |
from fastmcp.client.transports import (
|
| 8 |
SSETransport,
|
|
@@ -136,7 +134,5 @@ async def test_multi_client(tmp_path: Path):
|
|
| 136 |
|
| 137 |
result_1 = await client.call_tool("test_1_add", {"a": 1, "b": 2})
|
| 138 |
result_2 = await client.call_tool("test_2_add", {"a": 1, "b": 2})
|
| 139 |
-
assert
|
| 140 |
-
assert
|
| 141 |
-
assert isinstance(result_2[0], TextContent)
|
| 142 |
-
assert result_2[0].text == "3"
|
|
|
|
| 1 |
import inspect
|
| 2 |
from pathlib import Path
|
| 3 |
|
|
|
|
|
|
|
| 4 |
from fastmcp.client.client import Client
|
| 5 |
from fastmcp.client.transports import (
|
| 6 |
SSETransport,
|
|
|
|
| 134 |
|
| 135 |
result_1 = await client.call_tool("test_1_add", {"a": 1, "b": 2})
|
| 136 |
result_2 = await client.call_tool("test_2_add", {"a": 1, "b": 2})
|
| 137 |
+
assert result_1[0].text == "3" # type: ignore[attr-dict]
|
| 138 |
+
assert result_2[0].text == "3" # type: ignore[attr-dict]
|
|
|
|
|
|