Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
7b03b73
1
Parent(s): b8612c0
Update docs and test
Browse files- docs/deployment/asgi.mdx +4 -4
- docs/deployment/running-server.mdx +4 -4
- docs/tutorials/rest-api.mdx +1 -1
- src/fastmcp/client/auth/oauth.py +1 -1
- src/fastmcp/utilities/mcp_config.py +3 -1
- tests/auth/providers/test_bearer.py +3 -3
- tests/auth/test_oauth_client.py +1 -1
- tests/client/test_client.py +7 -5
- tests/client/test_openapi.py +3 -3
- tests/client/test_sse.py +3 -3
- tests/client/test_streamable_http.py +4 -4
- tests/deprecated/test_settings.py +2 -2
- tests/server/http/test_custom_routes.py +1 -1
- tests/server/http/test_http_dependencies.py +2 -2
- tests/server/http/test_http_middleware.py +1 -1
- tests/server/test_app_state.py +2 -2
- tests/server/test_mount.py +3 -1
- tests/server/test_proxy.py +2 -2
- tests/utilities/test_mcp_config.py +4 -4
docs/deployment/asgi.mdx
CHANGED
|
@@ -48,7 +48,7 @@ Both approaches return a Starlette application that can be integrated with other
|
|
| 48 |
The returned app stores the `FastMCP` instance on `app.state.fastmcp_server`, so you
|
| 49 |
can access it from custom middleware or routes via `request.app.state.fastmcp_server`.
|
| 50 |
|
| 51 |
-
The MCP server's endpoint is mounted at the root path `/mcp` for Streamable HTTP transport, and `/sse` for SSE transport, though you can change these paths by passing a `path` argument to the `http_app()` method:
|
| 52 |
|
| 53 |
```python
|
| 54 |
# For Streamable HTTP transport
|
|
@@ -137,7 +137,7 @@ app = Starlette(
|
|
| 137 |
)
|
| 138 |
```
|
| 139 |
|
| 140 |
-
The MCP endpoint will be available at `/mcp-server/mcp` of the resulting Starlette app.
|
| 141 |
|
| 142 |
<Warning>
|
| 143 |
For Streamable HTTP transport, you **must** pass the lifespan context from the FastMCP app to the resulting Starlette app, as nested lifespans are not recognized. Otherwise, the FastMCP server's session manager will not be properly initialized.
|
|
@@ -167,7 +167,7 @@ app = Starlette(
|
|
| 167 |
)
|
| 168 |
```
|
| 169 |
|
| 170 |
-
In this setup, the MCP server is accessible at the `/outer/inner/mcp` path of the resulting Starlette app.
|
| 171 |
|
| 172 |
<Warning>
|
| 173 |
For Streamable HTTP transport, you **must** pass the lifespan context from the FastMCP app to the *outer* Starlette app, as nested lifespans are not recognized. Otherwise, the FastMCP server's session manager will not be properly initialized.
|
|
@@ -194,7 +194,7 @@ app = FastAPI(lifespan=mcp_app.lifespan)
|
|
| 194 |
app.mount("/mcp-server", mcp_app)
|
| 195 |
```
|
| 196 |
|
| 197 |
-
The MCP endpoint will be available at `/mcp-server/mcp` of the resulting FastAPI app.
|
| 198 |
|
| 199 |
<Warning>
|
| 200 |
For Streamable HTTP transport, you **must** pass the lifespan context from the FastMCP app to the resulting FastAPI app, as nested lifespans are not recognized. Otherwise, the FastMCP server's session manager will not be properly initialized.
|
|
|
|
| 48 |
The returned app stores the `FastMCP` instance on `app.state.fastmcp_server`, so you
|
| 49 |
can access it from custom middleware or routes via `request.app.state.fastmcp_server`.
|
| 50 |
|
| 51 |
+
The MCP server's endpoint is mounted at the root path `/mcp/` for Streamable HTTP transport, and `/sse/` for SSE transport, though you can change these paths by passing a `path` argument to the `http_app()` method:
|
| 52 |
|
| 53 |
```python
|
| 54 |
# For Streamable HTTP transport
|
|
|
|
| 137 |
)
|
| 138 |
```
|
| 139 |
|
| 140 |
+
The MCP endpoint will be available at `/mcp-server/mcp/` of the resulting Starlette app.
|
| 141 |
|
| 142 |
<Warning>
|
| 143 |
For Streamable HTTP transport, you **must** pass the lifespan context from the FastMCP app to the resulting Starlette app, as nested lifespans are not recognized. Otherwise, the FastMCP server's session manager will not be properly initialized.
|
|
|
|
| 167 |
)
|
| 168 |
```
|
| 169 |
|
| 170 |
+
In this setup, the MCP server is accessible at the `/outer/inner/mcp/` path of the resulting Starlette app.
|
| 171 |
|
| 172 |
<Warning>
|
| 173 |
For Streamable HTTP transport, you **must** pass the lifespan context from the FastMCP app to the *outer* Starlette app, as nested lifespans are not recognized. Otherwise, the FastMCP server's session manager will not be properly initialized.
|
|
|
|
| 194 |
app.mount("/mcp-server", mcp_app)
|
| 195 |
```
|
| 196 |
|
| 197 |
+
The MCP endpoint will be available at `/mcp-server/mcp/` of the resulting FastAPI app.
|
| 198 |
|
| 199 |
<Warning>
|
| 200 |
For Streamable HTTP transport, you **must** pass the lifespan context from the FastMCP app to the resulting FastAPI app, as nested lifespans are not recognized. Otherwise, the FastMCP server's session manager will not be properly initialized.
|
docs/deployment/running-server.mdx
CHANGED
|
@@ -105,7 +105,7 @@ When using Stdio transport, you will typically *not* run the server yourself as
|
|
| 105 |
|
| 106 |
Streamable HTTP is a modern, efficient transport for exposing your MCP server via HTTP. It is the recommended transport for web-based deployments.
|
| 107 |
|
| 108 |
-
To run a server using Streamable HTTP, you can use the `run()` method with the `transport` argument set to `"streamable-http"`. This will start a Uvicorn server on the default host (`127.0.0.1`), port (`8000`), and path (`/mcp`).
|
| 109 |
<CodeGroup>
|
| 110 |
```python {6} server.py
|
| 111 |
from fastmcp import FastMCP
|
|
@@ -120,7 +120,7 @@ import asyncio
|
|
| 120 |
from fastmcp import Client
|
| 121 |
|
| 122 |
async def example():
|
| 123 |
-
async with Client("http://127.0.0.1:8000/mcp") as client:
|
| 124 |
await client.ping()
|
| 125 |
|
| 126 |
if __name__ == "__main__":
|
|
@@ -168,7 +168,7 @@ New applications should use Streamable HTTP transport instead.
|
|
| 168 |
|
| 169 |
Server-Sent Events (SSE) is an HTTP-based protocol for server-to-client streaming. While FastMCP still supports SSE, it is deprecated and Streamable HTTP is preferred for new projects.
|
| 170 |
|
| 171 |
-
To run a server using SSE, you can use the `run()` method with the `transport` argument set to `"sse"`. This will start a Uvicorn server on the default host (`127.0.0.1`), port (`8000`), and with default SSE path (`/sse`) and message path (`/messages/`).
|
| 172 |
|
| 173 |
<CodeGroup>
|
| 174 |
```python {6} server.py
|
|
@@ -186,7 +186,7 @@ from fastmcp.client.transports import SSETransport
|
|
| 186 |
|
| 187 |
async def example():
|
| 188 |
async with Client(
|
| 189 |
-
transport=SSETransport("http://127.0.0.1:8000/sse")
|
| 190 |
) as client:
|
| 191 |
await client.ping()
|
| 192 |
|
|
|
|
| 105 |
|
| 106 |
Streamable HTTP is a modern, efficient transport for exposing your MCP server via HTTP. It is the recommended transport for web-based deployments.
|
| 107 |
|
| 108 |
+
To run a server using Streamable HTTP, you can use the `run()` method with the `transport` argument set to `"streamable-http"`. This will start a Uvicorn server on the default host (`127.0.0.1`), port (`8000`), and path (`/mcp/`).
|
| 109 |
<CodeGroup>
|
| 110 |
```python {6} server.py
|
| 111 |
from fastmcp import FastMCP
|
|
|
|
| 120 |
from fastmcp import Client
|
| 121 |
|
| 122 |
async def example():
|
| 123 |
+
async with Client("http://127.0.0.1:8000/mcp/") as client:
|
| 124 |
await client.ping()
|
| 125 |
|
| 126 |
if __name__ == "__main__":
|
|
|
|
| 168 |
|
| 169 |
Server-Sent Events (SSE) is an HTTP-based protocol for server-to-client streaming. While FastMCP still supports SSE, it is deprecated and Streamable HTTP is preferred for new projects.
|
| 170 |
|
| 171 |
+
To run a server using SSE, you can use the `run()` method with the `transport` argument set to `"sse"`. This will start a Uvicorn server on the default host (`127.0.0.1`), port (`8000`), and with default SSE path (`/sse/`) and message path (`/messages/`).
|
| 172 |
|
| 173 |
<CodeGroup>
|
| 174 |
```python {6} server.py
|
|
|
|
| 186 |
|
| 187 |
async def example():
|
| 188 |
async with Client(
|
| 189 |
+
transport=SSETransport("http://127.0.0.1:8000/sse/")
|
| 190 |
) as client:
|
| 191 |
await client.ping()
|
| 192 |
|
docs/tutorials/rest-api.mdx
CHANGED
|
@@ -103,7 +103,7 @@ from fastmcp import Client
|
|
| 103 |
|
| 104 |
async def main():
|
| 105 |
# Connect to the MCP server we just created
|
| 106 |
-
async with Client("http://127.0.0.1:8000/mcp") as client:
|
| 107 |
|
| 108 |
# List the tools that were automatically generated
|
| 109 |
tools = await client.list_tools()
|
|
|
|
| 103 |
|
| 104 |
async def main():
|
| 105 |
# Connect to the MCP server we just created
|
| 106 |
+
async with Client("http://127.0.0.1:8000/mcp/") as client:
|
| 107 |
|
| 108 |
# List the tools that were automatically generated
|
| 109 |
tools = await client.list_tools()
|
src/fastmcp/client/auth/oauth.py
CHANGED
|
@@ -307,7 +307,7 @@ def OAuth(
|
|
| 307 |
|
| 308 |
Args:
|
| 309 |
mcp_url: Full URL to the MCP endpoint (e.g.,
|
| 310 |
-
"http://host/mcp/sse")
|
| 311 |
scopes: OAuth scopes to request. Can be a
|
| 312 |
space-separated string or a list of strings.
|
| 313 |
client_name: Name for this client during registration
|
|
|
|
| 307 |
|
| 308 |
Args:
|
| 309 |
mcp_url: Full URL to the MCP endpoint (e.g.,
|
| 310 |
+
"http://host/mcp/sse/")
|
| 311 |
scopes: OAuth scopes to request. Can be a
|
| 312 |
space-separated string or a list of strings.
|
| 313 |
client_name: Name for this client during registration
|
src/fastmcp/utilities/mcp_config.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
|
|
|
| 3 |
from typing import TYPE_CHECKING, Annotated, Any, Literal
|
| 4 |
from urllib.parse import urlparse
|
| 5 |
|
|
@@ -28,7 +29,8 @@ def infer_transport_type_from_url(
|
|
| 28 |
parsed_url = urlparse(url)
|
| 29 |
path = parsed_url.path
|
| 30 |
|
| 31 |
-
|
|
|
|
| 32 |
return "sse"
|
| 33 |
else:
|
| 34 |
return "streamable-http"
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
+
import re
|
| 4 |
from typing import TYPE_CHECKING, Annotated, Any, Literal
|
| 5 |
from urllib.parse import urlparse
|
| 6 |
|
|
|
|
| 29 |
parsed_url = urlparse(url)
|
| 30 |
path = parsed_url.path
|
| 31 |
|
| 32 |
+
# Match /sse followed by /, ?, &, or end of string
|
| 33 |
+
if re.search(r"/sse(/|\?|&|$)", path):
|
| 34 |
return "sse"
|
| 35 |
else:
|
| 36 |
return "streamable-http"
|
tests/auth/providers/test_bearer.py
CHANGED
|
@@ -67,7 +67,7 @@ def mcp_server_url(rsa_key_pair: RSAKeyPair) -> Generator[str]:
|
|
| 67 |
public_key=rsa_key_pair.public_key,
|
| 68 |
run_kwargs=dict(transport="streamable-http"),
|
| 69 |
) as url:
|
| 70 |
-
yield f"{url}/mcp"
|
| 71 |
|
| 72 |
|
| 73 |
class TestRSAKeyPair:
|
|
@@ -698,7 +698,7 @@ class TestFastMCPBearerAuth:
|
|
| 698 |
auth_kwargs=dict(required_scopes=["read", "write"]),
|
| 699 |
run_kwargs=dict(transport="streamable-http"),
|
| 700 |
) as url:
|
| 701 |
-
mcp_server_url = f"{url}/mcp"
|
| 702 |
with pytest.raises(httpx.HTTPStatusError) as exc_info:
|
| 703 |
async with Client(mcp_server_url, auth=BearerAuth(token)) as client:
|
| 704 |
tools = await client.list_tools() # noqa: F841
|
|
@@ -721,7 +721,7 @@ class TestFastMCPBearerAuth:
|
|
| 721 |
auth_kwargs=dict(required_scopes=["read", "write"]),
|
| 722 |
run_kwargs=dict(transport="streamable-http"),
|
| 723 |
) as url:
|
| 724 |
-
mcp_server_url = f"{url}/mcp"
|
| 725 |
async with Client(mcp_server_url, auth=BearerAuth(token)) as client:
|
| 726 |
tools = await client.list_tools()
|
| 727 |
assert tools
|
|
|
|
| 67 |
public_key=rsa_key_pair.public_key,
|
| 68 |
run_kwargs=dict(transport="streamable-http"),
|
| 69 |
) as url:
|
| 70 |
+
yield f"{url}/mcp/"
|
| 71 |
|
| 72 |
|
| 73 |
class TestRSAKeyPair:
|
|
|
|
| 698 |
auth_kwargs=dict(required_scopes=["read", "write"]),
|
| 699 |
run_kwargs=dict(transport="streamable-http"),
|
| 700 |
) as url:
|
| 701 |
+
mcp_server_url = f"{url}/mcp/"
|
| 702 |
with pytest.raises(httpx.HTTPStatusError) as exc_info:
|
| 703 |
async with Client(mcp_server_url, auth=BearerAuth(token)) as client:
|
| 704 |
tools = await client.list_tools() # noqa: F841
|
|
|
|
| 721 |
auth_kwargs=dict(required_scopes=["read", "write"]),
|
| 722 |
run_kwargs=dict(transport="streamable-http"),
|
| 723 |
) as url:
|
| 724 |
+
mcp_server_url = f"{url}/mcp/"
|
| 725 |
async with Client(mcp_server_url, auth=BearerAuth(token)) as client:
|
| 726 |
tools = await client.list_tools()
|
| 727 |
assert tools
|
tests/auth/test_oauth_client.py
CHANGED
|
@@ -44,7 +44,7 @@ def run_server(host: str, port: int, **kwargs) -> None:
|
|
| 44 |
@pytest.fixture(scope="module")
|
| 45 |
def streamable_http_server() -> Generator[str, None, None]:
|
| 46 |
with run_server_in_process(run_server, transport="streamable-http") as url:
|
| 47 |
-
yield f"{url}/mcp"
|
| 48 |
|
| 49 |
|
| 50 |
@pytest.fixture()
|
|
|
|
| 44 |
@pytest.fixture(scope="module")
|
| 45 |
def streamable_http_server() -> Generator[str, None, None]:
|
| 46 |
with run_server_in_process(run_server, transport="streamable-http") as url:
|
| 47 |
+
yield f"{url}/mcp/"
|
| 48 |
|
| 49 |
|
| 50 |
@pytest.fixture()
|
tests/client/test_client.py
CHANGED
|
@@ -735,7 +735,8 @@ class TestInferTransport:
|
|
| 735 |
"http://example.com/api/sse/stream",
|
| 736 |
"https://localhost:8080/mcp/sse/endpoint",
|
| 737 |
"http://example.com/api/sse",
|
| 738 |
-
"
|
|
|
|
| 739 |
"http://example.com/api/sse?param=value",
|
| 740 |
"https://localhost:8080/mcp/sse/?param=value",
|
| 741 |
"https://localhost:8000/mcp/sse?x=1&y=2",
|
|
@@ -744,6 +745,7 @@ class TestInferTransport:
|
|
| 744 |
"path_with_sse_directory",
|
| 745 |
"path_with_sse_subdirectory",
|
| 746 |
"path_ending_with_sse",
|
|
|
|
| 747 |
"path_ending_with_sse_https",
|
| 748 |
"path_with_sse_and_query_params",
|
| 749 |
"path_with_sse_slash_and_query_params",
|
|
@@ -758,7 +760,7 @@ class TestInferTransport:
|
|
| 758 |
"url",
|
| 759 |
[
|
| 760 |
"http://example.com/api",
|
| 761 |
-
"https://localhost:8080/mcp",
|
| 762 |
"http://example.com/asset/image.jpg",
|
| 763 |
"https://localhost:8080/sservice/endpoint",
|
| 764 |
"https://example.com/assets/file",
|
|
@@ -779,7 +781,7 @@ class TestInferTransport:
|
|
| 779 |
config = {
|
| 780 |
"mcpServers": {
|
| 781 |
"test_server": {
|
| 782 |
-
"url": "http://localhost:8000/sse",
|
| 783 |
"headers": {"Authorization": "Bearer 123"},
|
| 784 |
},
|
| 785 |
}
|
|
@@ -787,7 +789,7 @@ class TestInferTransport:
|
|
| 787 |
transport = infer_transport(config)
|
| 788 |
assert isinstance(transport, MCPConfigTransport)
|
| 789 |
assert isinstance(transport.transport, SSETransport)
|
| 790 |
-
assert transport.transport.url == "http://localhost:8000/sse"
|
| 791 |
assert transport.transport.headers == {"Authorization": "Bearer 123"}
|
| 792 |
|
| 793 |
def test_infer_local_transport_from_config(self):
|
|
@@ -825,7 +827,7 @@ class TestInferTransport:
|
|
| 825 |
"args": ["hello"],
|
| 826 |
},
|
| 827 |
"remote": {
|
| 828 |
-
"url": "http://localhost:8000/sse",
|
| 829 |
"headers": {"Authorization": "Bearer 123"},
|
| 830 |
},
|
| 831 |
}
|
|
|
|
| 735 |
"http://example.com/api/sse/stream",
|
| 736 |
"https://localhost:8080/mcp/sse/endpoint",
|
| 737 |
"http://example.com/api/sse",
|
| 738 |
+
"http://example.com/api/sse/",
|
| 739 |
+
"https://localhost:8080/mcp/sse/",
|
| 740 |
"http://example.com/api/sse?param=value",
|
| 741 |
"https://localhost:8080/mcp/sse/?param=value",
|
| 742 |
"https://localhost:8000/mcp/sse?x=1&y=2",
|
|
|
|
| 745 |
"path_with_sse_directory",
|
| 746 |
"path_with_sse_subdirectory",
|
| 747 |
"path_ending_with_sse",
|
| 748 |
+
"path_ending_with_sse_slash",
|
| 749 |
"path_ending_with_sse_https",
|
| 750 |
"path_with_sse_and_query_params",
|
| 751 |
"path_with_sse_slash_and_query_params",
|
|
|
|
| 760 |
"url",
|
| 761 |
[
|
| 762 |
"http://example.com/api",
|
| 763 |
+
"https://localhost:8080/mcp/",
|
| 764 |
"http://example.com/asset/image.jpg",
|
| 765 |
"https://localhost:8080/sservice/endpoint",
|
| 766 |
"https://example.com/assets/file",
|
|
|
|
| 781 |
config = {
|
| 782 |
"mcpServers": {
|
| 783 |
"test_server": {
|
| 784 |
+
"url": "http://localhost:8000/sse/",
|
| 785 |
"headers": {"Authorization": "Bearer 123"},
|
| 786 |
},
|
| 787 |
}
|
|
|
|
| 789 |
transport = infer_transport(config)
|
| 790 |
assert isinstance(transport, MCPConfigTransport)
|
| 791 |
assert isinstance(transport.transport, SSETransport)
|
| 792 |
+
assert transport.transport.url == "http://localhost:8000/sse/"
|
| 793 |
assert transport.transport.headers == {"Authorization": "Bearer 123"}
|
| 794 |
|
| 795 |
def test_infer_local_transport_from_config(self):
|
|
|
|
| 827 |
"args": ["hello"],
|
| 828 |
},
|
| 829 |
"remote": {
|
| 830 |
+
"url": "http://localhost:8000/sse/",
|
| 831 |
"headers": {"Authorization": "Bearer 123"},
|
| 832 |
},
|
| 833 |
}
|
tests/client/test_openapi.py
CHANGED
|
@@ -57,12 +57,12 @@ class TestClientHeaders:
|
|
| 57 |
@pytest.fixture(scope="class")
|
| 58 |
def shttp_server(self) -> Generator[str, None, None]:
|
| 59 |
with run_server_in_process(run_server, transport="streamable-http") as url:
|
| 60 |
-
yield f"{url}/mcp"
|
| 61 |
|
| 62 |
@pytest.fixture(scope="class")
|
| 63 |
def sse_server(self) -> Generator[str, None, None]:
|
| 64 |
with run_server_in_process(run_server, transport="sse") as url:
|
| 65 |
-
yield f"{url}/sse"
|
| 66 |
|
| 67 |
@pytest.fixture(scope="class")
|
| 68 |
def proxy_server(self, shttp_server: str) -> Generator[str, None, None]:
|
|
@@ -71,7 +71,7 @@ class TestClientHeaders:
|
|
| 71 |
shttp_url=shttp_server,
|
| 72 |
transport="streamable-http",
|
| 73 |
) as url:
|
| 74 |
-
yield f"{url}/mcp"
|
| 75 |
|
| 76 |
async def test_client_headers_sse_resource(self, sse_server: str):
|
| 77 |
async with Client(
|
|
|
|
| 57 |
@pytest.fixture(scope="class")
|
| 58 |
def shttp_server(self) -> Generator[str, None, None]:
|
| 59 |
with run_server_in_process(run_server, transport="streamable-http") as url:
|
| 60 |
+
yield f"{url}/mcp/"
|
| 61 |
|
| 62 |
@pytest.fixture(scope="class")
|
| 63 |
def sse_server(self) -> Generator[str, None, None]:
|
| 64 |
with run_server_in_process(run_server, transport="sse") as url:
|
| 65 |
+
yield f"{url}/sse/"
|
| 66 |
|
| 67 |
@pytest.fixture(scope="class")
|
| 68 |
def proxy_server(self, shttp_server: str) -> Generator[str, None, None]:
|
|
|
|
| 71 |
shttp_url=shttp_server,
|
| 72 |
transport="streamable-http",
|
| 73 |
) as url:
|
| 74 |
+
yield f"{url}/mcp/"
|
| 75 |
|
| 76 |
async def test_client_headers_sse_resource(self, sse_server: str):
|
| 77 |
async with Client(
|
tests/client/test_sse.py
CHANGED
|
@@ -70,7 +70,7 @@ def run_server(host: str, port: int, **kwargs) -> None:
|
|
| 70 |
@pytest.fixture(autouse=True, scope="module")
|
| 71 |
def sse_server() -> Generator[str, None, None]:
|
| 72 |
with run_server_in_process(run_server, transport="sse") as url:
|
| 73 |
-
yield f"{url}/sse"
|
| 74 |
|
| 75 |
|
| 76 |
async def test_ping(sse_server: str):
|
|
@@ -92,7 +92,7 @@ async def test_http_headers(sse_server: str):
|
|
| 92 |
|
| 93 |
|
| 94 |
def run_nested_server(host: str, port: int) -> None:
|
| 95 |
-
app = fastmcp_server().sse_app(path="/mcp/sse", message_path="/mcp/messages")
|
| 96 |
mount = Starlette(routes=[Mount("/nest-inner", app=app)])
|
| 97 |
mount2 = Starlette(routes=[Mount("/nest-outer", app=mount)])
|
| 98 |
server = uvicorn.Server(
|
|
@@ -114,7 +114,7 @@ async def test_nested_sse_server_resolves_correctly():
|
|
| 114 |
|
| 115 |
with run_server_in_process(run_nested_server) as url:
|
| 116 |
async with Client(
|
| 117 |
-
transport=SSETransport(f"{url}/nest-outer/nest-inner/mcp/sse")
|
| 118 |
) as client:
|
| 119 |
result = await client.ping()
|
| 120 |
assert result is True
|
|
|
|
| 70 |
@pytest.fixture(autouse=True, scope="module")
|
| 71 |
def sse_server() -> Generator[str, None, None]:
|
| 72 |
with run_server_in_process(run_server, transport="sse") as url:
|
| 73 |
+
yield f"{url}/sse/"
|
| 74 |
|
| 75 |
|
| 76 |
async def test_ping(sse_server: str):
|
|
|
|
| 92 |
|
| 93 |
|
| 94 |
def run_nested_server(host: str, port: int) -> None:
|
| 95 |
+
app = fastmcp_server().sse_app(path="/mcp/sse/", message_path="/mcp/messages")
|
| 96 |
mount = Starlette(routes=[Mount("/nest-inner", app=app)])
|
| 97 |
mount2 = Starlette(routes=[Mount("/nest-outer", app=mount)])
|
| 98 |
server = uvicorn.Server(
|
|
|
|
| 114 |
|
| 115 |
with run_server_in_process(run_nested_server) as url:
|
| 116 |
async with Client(
|
| 117 |
+
transport=SSETransport(f"{url}/nest-outer/nest-inner/mcp/sse/")
|
| 118 |
) as client:
|
| 119 |
result = await client.ping()
|
| 120 |
assert result is True
|
tests/client/test_streamable_http.py
CHANGED
|
@@ -79,7 +79,7 @@ def run_server(host: str, port: int, stateless_http: bool = False, **kwargs) ->
|
|
| 79 |
|
| 80 |
|
| 81 |
def run_nested_server(host: str, port: int) -> None:
|
| 82 |
-
mcp_app = fastmcp_server().http_app(path="/final/mcp")
|
| 83 |
|
| 84 |
mount = Starlette(routes=[Mount("/nest-inner", app=mcp_app)])
|
| 85 |
mount2 = Starlette(
|
|
@@ -105,9 +105,9 @@ async def streamable_http_server(
|
|
| 105 |
with run_server_in_process(
|
| 106 |
run_server, stateless_http=stateless_http, transport="streamable-http"
|
| 107 |
) as url:
|
| 108 |
-
async with Client(transport=StreamableHttpTransport(f"{url}/mcp")) as client:
|
| 109 |
assert await client.ping()
|
| 110 |
-
yield f"{url}/mcp"
|
| 111 |
|
| 112 |
|
| 113 |
async def test_ping(streamable_http_server: str):
|
|
@@ -156,7 +156,7 @@ async def test_nested_streamable_http_server_resolves_correctly():
|
|
| 156 |
|
| 157 |
with run_server_in_process(run_nested_server) as url:
|
| 158 |
async with Client(
|
| 159 |
-
transport=StreamableHttpTransport(f"{url}/nest-outer/nest-inner/final/mcp")
|
| 160 |
) as client:
|
| 161 |
result = await client.ping()
|
| 162 |
assert result is True
|
|
|
|
| 79 |
|
| 80 |
|
| 81 |
def run_nested_server(host: str, port: int) -> None:
|
| 82 |
+
mcp_app = fastmcp_server().http_app(path="/final/mcp/")
|
| 83 |
|
| 84 |
mount = Starlette(routes=[Mount("/nest-inner", app=mcp_app)])
|
| 85 |
mount2 = Starlette(
|
|
|
|
| 105 |
with run_server_in_process(
|
| 106 |
run_server, stateless_http=stateless_http, transport="streamable-http"
|
| 107 |
) as url:
|
| 108 |
+
async with Client(transport=StreamableHttpTransport(f"{url}/mcp/")) as client:
|
| 109 |
assert await client.ping()
|
| 110 |
+
yield f"{url}/mcp/"
|
| 111 |
|
| 112 |
|
| 113 |
async def test_ping(streamable_http_server: str):
|
|
|
|
| 156 |
|
| 157 |
with run_server_in_process(run_nested_server) as url:
|
| 158 |
async with Client(
|
| 159 |
+
transport=StreamableHttpTransport(f"{url}/nest-outer/nest-inner/final/mcp/")
|
| 160 |
) as client:
|
| 161 |
result = await client.ping()
|
| 162 |
assert result is True
|
tests/deprecated/test_settings.py
CHANGED
|
@@ -123,7 +123,7 @@ class TestDeprecatedServerInitKwargs:
|
|
| 123 |
debug=False,
|
| 124 |
host="127.0.0.1",
|
| 125 |
port=9999,
|
| 126 |
-
sse_path="/sse",
|
| 127 |
message_path="/msg",
|
| 128 |
streamable_http_path="/http",
|
| 129 |
json_response=False,
|
|
@@ -162,7 +162,7 @@ class TestDeprecatedServerInitKwargs:
|
|
| 162 |
assert server._deprecated_settings.debug is False
|
| 163 |
assert server._deprecated_settings.host == "127.0.0.1"
|
| 164 |
assert server._deprecated_settings.port == 9999
|
| 165 |
-
assert server._deprecated_settings.sse_path == "/sse"
|
| 166 |
assert server._deprecated_settings.message_path == "/msg"
|
| 167 |
assert server._deprecated_settings.streamable_http_path == "/http"
|
| 168 |
assert server._deprecated_settings.json_response is False
|
|
|
|
| 123 |
debug=False,
|
| 124 |
host="127.0.0.1",
|
| 125 |
port=9999,
|
| 126 |
+
sse_path="/sse/",
|
| 127 |
message_path="/msg",
|
| 128 |
streamable_http_path="/http",
|
| 129 |
json_response=False,
|
|
|
|
| 162 |
assert server._deprecated_settings.debug is False
|
| 163 |
assert server._deprecated_settings.host == "127.0.0.1"
|
| 164 |
assert server._deprecated_settings.port == 9999
|
| 165 |
+
assert server._deprecated_settings.sse_path == "/sse/"
|
| 166 |
assert server._deprecated_settings.message_path == "/msg"
|
| 167 |
assert server._deprecated_settings.streamable_http_path == "/http"
|
| 168 |
assert server._deprecated_settings.json_response is False
|
tests/server/http/test_custom_routes.py
CHANGED
|
@@ -55,7 +55,7 @@ class TestCustomRoutes:
|
|
| 55 |
"""Test that custom routes are included when using create_sse_app directly."""
|
| 56 |
# Create the app by calling the constructor function directly
|
| 57 |
app = create_sse_app(
|
| 58 |
-
server=server_with_custom_route, message_path="/message", sse_path="/sse"
|
| 59 |
)
|
| 60 |
|
| 61 |
# Verify that the custom route is included
|
|
|
|
| 55 |
"""Test that custom routes are included when using create_sse_app directly."""
|
| 56 |
# Create the app by calling the constructor function directly
|
| 57 |
app = create_sse_app(
|
| 58 |
+
server=server_with_custom_route, message_path="/message", sse_path="/sse/"
|
| 59 |
)
|
| 60 |
|
| 61 |
# Verify that the custom route is included
|
tests/server/http/test_http_dependencies.py
CHANGED
|
@@ -45,13 +45,13 @@ def run_server(host: str, port: int, **kwargs) -> None:
|
|
| 45 |
@pytest.fixture(autouse=True, scope="module")
|
| 46 |
def shttp_server() -> Generator[str, None, None]:
|
| 47 |
with run_server_in_process(run_server, transport="streamable-http") as url:
|
| 48 |
-
yield f"{url}/mcp"
|
| 49 |
|
| 50 |
|
| 51 |
@pytest.fixture(autouse=True, scope="module")
|
| 52 |
def sse_server() -> Generator[str, None, None]:
|
| 53 |
with run_server_in_process(run_server, transport="sse") as url:
|
| 54 |
-
yield f"{url}/sse"
|
| 55 |
|
| 56 |
|
| 57 |
async def test_http_headers_resource_shttp(shttp_server: str):
|
|
|
|
| 45 |
@pytest.fixture(autouse=True, scope="module")
|
| 46 |
def shttp_server() -> Generator[str, None, None]:
|
| 47 |
with run_server_in_process(run_server, transport="streamable-http") as url:
|
| 48 |
+
yield f"{url}/mcp/"
|
| 49 |
|
| 50 |
|
| 51 |
@pytest.fixture(autouse=True, scope="module")
|
| 52 |
def sse_server() -> Generator[str, None, None]:
|
| 53 |
with run_server_in_process(run_server, transport="sse") as url:
|
| 54 |
+
yield f"{url}/sse/"
|
| 55 |
|
| 56 |
|
| 57 |
async def test_http_headers_resource_shttp(shttp_server: str):
|
tests/server/http/test_http_middleware.py
CHANGED
|
@@ -126,7 +126,7 @@ async def test_create_sse_app_with_custom_middleware():
|
|
| 126 |
app = create_sse_app(
|
| 127 |
server=server,
|
| 128 |
message_path="/message",
|
| 129 |
-
sse_path="/sse",
|
| 130 |
middleware=custom_middleware,
|
| 131 |
routes=additional_routes,
|
| 132 |
)
|
|
|
|
| 126 |
app = create_sse_app(
|
| 127 |
server=server,
|
| 128 |
message_path="/message",
|
| 129 |
+
sse_path="/sse/",
|
| 130 |
middleware=custom_middleware,
|
| 131 |
routes=additional_routes,
|
| 132 |
)
|
tests/server/test_app_state.py
CHANGED
|
@@ -16,11 +16,11 @@ def test_http_app_sse_sets_mcp_server_state():
|
|
| 16 |
|
| 17 |
def test_create_streamable_http_app_sets_state():
|
| 18 |
server = FastMCP(name="StateTest")
|
| 19 |
-
app = create_streamable_http_app(server, "/mcp")
|
| 20 |
assert app.state.fastmcp_server is server
|
| 21 |
|
| 22 |
|
| 23 |
def test_create_sse_app_sets_state():
|
| 24 |
server = FastMCP(name="StateTest")
|
| 25 |
-
app = create_sse_app(server, message_path="/message", sse_path="/sse")
|
| 26 |
assert app.state.fastmcp_server is server
|
|
|
|
| 16 |
|
| 17 |
def test_create_streamable_http_app_sets_state():
|
| 18 |
server = FastMCP(name="StateTest")
|
| 19 |
+
app = create_streamable_http_app(server, "/mcp/")
|
| 20 |
assert app.state.fastmcp_server is server
|
| 21 |
|
| 22 |
|
| 23 |
def test_create_sse_app_sets_state():
|
| 24 |
server = FastMCP(name="StateTest")
|
| 25 |
+
app = create_sse_app(server, message_path="/message", sse_path="/sse/")
|
| 26 |
assert app.state.fastmcp_server is server
|
tests/server/test_mount.py
CHANGED
|
@@ -273,7 +273,9 @@ class TestMultipleServerMount:
|
|
| 273 |
main_app.mount(working_app, "working")
|
| 274 |
|
| 275 |
# Use an unreachable port
|
| 276 |
-
unreachable_client = Client(
|
|
|
|
|
|
|
| 277 |
|
| 278 |
# Create a proxy server that will fail to connect
|
| 279 |
unreachable_proxy = FastMCP.as_proxy(unreachable_client)
|
|
|
|
| 273 |
main_app.mount(working_app, "working")
|
| 274 |
|
| 275 |
# Use an unreachable port
|
| 276 |
+
unreachable_client = Client(
|
| 277 |
+
transport=SSETransport("http://127.0.0.1:9999/sse/")
|
| 278 |
+
)
|
| 279 |
|
| 280 |
# Create a proxy server that will fail to connect
|
| 281 |
unreachable_proxy = FastMCP.as_proxy(unreachable_client)
|
tests/server/test_proxy.py
CHANGED
|
@@ -102,10 +102,10 @@ async def test_as_proxy_with_transport(fastmcp_server):
|
|
| 102 |
|
| 103 |
def test_as_proxy_with_url():
|
| 104 |
"""FastMCP.as_proxy should accept a URL without connecting."""
|
| 105 |
-
proxy = FastMCP.as_proxy("http://example.com/mcp")
|
| 106 |
assert isinstance(proxy, FastMCPProxy)
|
| 107 |
assert isinstance(proxy.client.transport, StreamableHttpTransport)
|
| 108 |
-
assert proxy.client.transport.url == "http://example.com/mcp"
|
| 109 |
|
| 110 |
|
| 111 |
class TestTools:
|
|
|
|
| 102 |
|
| 103 |
def test_as_proxy_with_url():
|
| 104 |
"""FastMCP.as_proxy should accept a URL without connecting."""
|
| 105 |
+
proxy = FastMCP.as_proxy("http://example.com/mcp/")
|
| 106 |
assert isinstance(proxy, FastMCPProxy)
|
| 107 |
assert isinstance(proxy.client.transport, StreamableHttpTransport)
|
| 108 |
+
assert proxy.client.transport.url == "http://example.com/mcp/"
|
| 109 |
|
| 110 |
|
| 111 |
class TestTools:
|
tests/utilities/test_mcp_config.py
CHANGED
|
@@ -61,21 +61,21 @@ def test_parse_remote_config_with_url_inference():
|
|
| 61 |
config = {
|
| 62 |
"mcpServers": {
|
| 63 |
"test_server": {
|
| 64 |
-
"url": "http://localhost:8000/sse",
|
| 65 |
}
|
| 66 |
}
|
| 67 |
}
|
| 68 |
mcp_config = MCPConfig.from_dict(config)
|
| 69 |
transport = mcp_config.mcpServers["test_server"].to_transport()
|
| 70 |
assert isinstance(transport, SSETransport)
|
| 71 |
-
assert transport.url == "http://localhost:8000/sse"
|
| 72 |
|
| 73 |
|
| 74 |
def test_parse_multiple_servers():
|
| 75 |
config = {
|
| 76 |
"mcpServers": {
|
| 77 |
"test_server": {
|
| 78 |
-
"url": "http://localhost:8000/sse",
|
| 79 |
},
|
| 80 |
"test_server_2": {
|
| 81 |
"command": "echo",
|
|
@@ -172,7 +172,7 @@ async def test_remote_config_sse_with_auth_token():
|
|
| 172 |
config = {
|
| 173 |
"mcpServers": {
|
| 174 |
"test_server": {
|
| 175 |
-
"url": "http://localhost:8000/sse",
|
| 176 |
"auth": "test_token",
|
| 177 |
}
|
| 178 |
}
|
|
|
|
| 61 |
config = {
|
| 62 |
"mcpServers": {
|
| 63 |
"test_server": {
|
| 64 |
+
"url": "http://localhost:8000/sse/",
|
| 65 |
}
|
| 66 |
}
|
| 67 |
}
|
| 68 |
mcp_config = MCPConfig.from_dict(config)
|
| 69 |
transport = mcp_config.mcpServers["test_server"].to_transport()
|
| 70 |
assert isinstance(transport, SSETransport)
|
| 71 |
+
assert transport.url == "http://localhost:8000/sse/"
|
| 72 |
|
| 73 |
|
| 74 |
def test_parse_multiple_servers():
|
| 75 |
config = {
|
| 76 |
"mcpServers": {
|
| 77 |
"test_server": {
|
| 78 |
+
"url": "http://localhost:8000/sse/",
|
| 79 |
},
|
| 80 |
"test_server_2": {
|
| 81 |
"command": "echo",
|
|
|
|
| 172 |
config = {
|
| 173 |
"mcpServers": {
|
| 174 |
"test_server": {
|
| 175 |
+
"url": "http://localhost:8000/sse/",
|
| 176 |
"auth": "test_token",
|
| 177 |
}
|
| 178 |
}
|