From fbe2fc353338e2c8e852b1c064e884c2163352f4 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 20 Jun 2025 13:12:23 -0400 Subject: [PATCH] Add transport handling for trailing slashes --- src/fastmcp/client/transports.py | 15 +++++++++++++++ tests/utilities/test_mcp_config.py | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/fastmcp/client/transports.py b/src/fastmcp/client/transports.py index 778c58447..153491029 100644 --- a/src/fastmcp/client/transports.py +++ b/src/fastmcp/client/transports.py @@ -9,6 +9,7 @@ import warnings from collections.abc import AsyncIterator, Callable from pathlib import Path from typing import Any, Literal, TypedDict, TypeVar, cast, overload +from urllib.parse import urlparse, urlunparse import anyio import httpx @@ -159,6 +160,13 @@ class SSETransport(ClientTransport): url = str(url) if not isinstance(url, str) or not url.startswith("http"): raise ValueError("Invalid HTTP/S URL provided for SSE.") + + # Ensure the URL path ends with a trailing slash to avoid automatic redirects + parsed = urlparse(url) + if not parsed.path.endswith("/"): + parsed = parsed._replace(path=parsed.path + "/") + url = urlunparse(parsed) + self.url = url self.headers = headers or {} self._set_auth(auth) @@ -227,6 +235,13 @@ class StreamableHttpTransport(ClientTransport): url = str(url) if not isinstance(url, str) or not url.startswith("http"): raise ValueError("Invalid HTTP/S URL provided for Streamable HTTP.") + + # Ensure the URL path ends with a trailing slash to avoid automatic redirects + parsed = urlparse(url) + if not parsed.path.endswith("/"): + parsed = parsed._replace(path=parsed.path + "/") + url = urlunparse(parsed) + self.url = url self.headers = headers or {} self._set_auth(auth) diff --git a/tests/utilities/test_mcp_config.py b/tests/utilities/test_mcp_config.py index ec6833b0a..7775e12bc 100644 --- a/tests/utilities/test_mcp_config.py +++ b/tests/utilities/test_mcp_config.py @@ -39,7 +39,7 @@ def test_parse_single_remote_config(): mcp_config = MCPConfig.from_dict(config) transport = mcp_config.mcpServers["test_server"].to_transport() assert isinstance(transport, StreamableHttpTransport) - assert transport.url == "http://localhost:8000" + assert transport.url == "http://localhost:8000/" def test_parse_remote_config_with_transport(): @@ -54,7 +54,7 @@ def test_parse_remote_config_with_transport(): mcp_config = MCPConfig.from_dict(config) transport = mcp_config.mcpServers["test_server"].to_transport() assert isinstance(transport, SSETransport) - assert transport.url == "http://localhost:8000" + assert transport.url == "http://localhost:8000/" def test_parse_remote_config_with_url_inference():