Jeremiah Lowin commited on
Commit
d4b51b1
·
1 Parent(s): a7c6f7e

Infer sse transport from url

Browse files
docs/clients/transports.mdx CHANGED
@@ -40,7 +40,7 @@ Streamable HTTP is the recommended transport for web-based deployments, providin
40
  #### Overview
41
 
42
  - **Class:** `fastmcp.client.transports.StreamableHttpTransport`
43
- - **Inferred From:** URLs starting with `http://` or `https://` (default for HTTP URLs since v2.3.0)
44
  - **Server Compatibility:** Works with FastMCP servers running in `streamable-http` mode
45
 
46
  #### Basic Usage
@@ -62,6 +62,15 @@ async def main():
62
  asyncio.run(main())
63
  ```
64
 
 
 
 
 
 
 
 
 
 
65
  #### Authentication with Headers
66
 
67
  For servers requiring authentication:
@@ -88,23 +97,19 @@ Server-Sent Events (SSE) is a transport that allows servers to push data to clie
88
  #### Overview
89
 
90
  - **Class:** `fastmcp.client.transports.SSETransport`
91
- - **Inferred From:** Not automatically inferred for HTTP URLs since v2.3.0 (must be explicitly specified)
92
  - **Server Compatibility:** Works with FastMCP servers running in `sse` mode
93
 
94
  #### Basic Usage
95
 
96
- Since v2.3.0, you must explicitly create an `SSETransport` for SSE connections:
97
 
98
  ```python
99
  from fastmcp import Client
100
- from fastmcp.client.transports import SSETransport
101
  import asyncio
102
 
103
- # Create an SSE transport
104
- transport = SSETransport(url="https://example.com/sse")
105
-
106
- # Pass the transport to the client
107
- client = Client(transport)
108
 
109
  async def main():
110
  async with client:
@@ -114,6 +119,15 @@ async def main():
114
  asyncio.run(main())
115
  ```
116
 
 
 
 
 
 
 
 
 
 
117
  #### Authentication with Headers
118
 
119
  SSE transport also supports custom headers for authentication:
 
40
  #### Overview
41
 
42
  - **Class:** `fastmcp.client.transports.StreamableHttpTransport`
43
+ - **Inferred From:** URLs starting with `http://` or `https://` (default for HTTP URLs since v2.3.0) that do not contain `/sse/` in the path
44
  - **Server Compatibility:** Works with FastMCP servers running in `streamable-http` mode
45
 
46
  #### Basic Usage
 
62
  asyncio.run(main())
63
  ```
64
 
65
+ You can also explicitly instantiate the transport:
66
+
67
+ ```python
68
+ from fastmcp.client.transports import StreamableHttpTransport
69
+
70
+ transport = StreamableHttpTransport(url="https://example.com/mcp")
71
+ client = Client(transport)
72
+ ```
73
+
74
  #### Authentication with Headers
75
 
76
  For servers requiring authentication:
 
97
  #### Overview
98
 
99
  - **Class:** `fastmcp.client.transports.SSETransport`
100
+ - **Inferred From:** HTTP URLs containing `/sse/` in the path
101
  - **Server Compatibility:** Works with FastMCP servers running in `sse` mode
102
 
103
  #### Basic Usage
104
 
105
+ The simplest way to use SSE is to let the transport be inferred from a URL with `/sse/` in the path:
106
 
107
  ```python
108
  from fastmcp import Client
 
109
  import asyncio
110
 
111
+ # The Client automatically uses SSETransport for URLs containing /sse/ in the path
112
+ client = Client("https://example.com/sse")
 
 
 
113
 
114
  async def main():
115
  async with client:
 
119
  asyncio.run(main())
120
  ```
121
 
122
+ You can also explicitly instantiate the transport for URLs that do not contain `/sse/` in the path or for more control:
123
+
124
+ ```python
125
+ from fastmcp.client.transports import SSETransport
126
+
127
+ transport = SSETransport(url="https://example.com/sse")
128
+ client = Client(transport)
129
+ ```
130
+
131
  #### Authentication with Headers
132
 
133
  SSE transport also supports custom headers for authentication:
src/fastmcp/client/transports.py CHANGED
@@ -1,14 +1,13 @@
1
  import abc
2
  import contextlib
3
  import datetime
4
- import inspect
5
  import os
6
  import shutil
7
  import sys
8
- import warnings
9
  from collections.abc import AsyncIterator
10
  from pathlib import Path
11
  from typing import Any, TypedDict, cast
 
12
 
13
  from mcp import ClientSession, StdioServerParameters
14
  from mcp.client.session import (
@@ -26,6 +25,9 @@ from pydantic import AnyUrl
26
  from typing_extensions import Unpack
27
 
28
  from fastmcp.server import FastMCP as FastMCPServer
 
 
 
29
 
30
 
31
  class SessionKwargs(TypedDict, total=False):
@@ -486,36 +488,29 @@ def infer_transport(
486
 
487
  # the transport is a FastMCP server
488
  elif isinstance(transport, FastMCPServer):
489
- return FastMCPTransport(mcp=transport)
490
 
491
  # the transport is a path to a script
492
  elif isinstance(transport, Path | str) and Path(transport).exists():
493
  if str(transport).endswith(".py"):
494
- return PythonStdioTransport(script_path=transport)
495
  elif str(transport).endswith(".js"):
496
- return NodeStdioTransport(script_path=transport)
497
  else:
498
  raise ValueError(f"Unsupported script type: {transport}")
499
 
500
  # the transport is an http(s) URL
501
  elif isinstance(transport, AnyUrl | str) and str(transport).startswith("http"):
502
- if str(transport).rstrip("/").endswith("/sse"):
503
- warnings.warn(
504
- inspect.cleandoc(
505
- """
506
- As of FastMCP 2.3.0, HTTP URLs are inferred to use Streamable HTTP.
507
- The provided URL ends in `/sse`, so you may encounter unexpected behavior.
508
- If you intended to use SSE, please use the `SSETransport` class directly.
509
- """
510
- ),
511
- category=UserWarning,
512
- stacklevel=2,
513
- )
514
- return StreamableHttpTransport(url=transport)
515
-
516
- # the transport is a websocket URL
517
- elif isinstance(transport, AnyUrl | str) and str(transport).startswith("ws"):
518
- return WSTransport(url=transport)
519
 
520
  ## if the transport is a config dict
521
  elif isinstance(transport, dict):
@@ -530,7 +525,7 @@ def infer_transport(
530
  server_name = list(server.keys())[0]
531
  # Stdio transport
532
  if "command" in server[server_name] and "args" in server[server_name]:
533
- return StdioTransport(
534
  command=server[server_name]["command"],
535
  args=server[server_name]["args"],
536
  env=server[server_name].get("env", None),
@@ -539,7 +534,7 @@ def infer_transport(
539
 
540
  # HTTP transport
541
  elif "url" in server:
542
- return SSETransport(
543
  url=server["url"],
544
  headers=server.get("headers", None),
545
  )
@@ -549,3 +544,6 @@ def infer_transport(
549
  # the transport is an unknown type
550
  else:
551
  raise ValueError(f"Could not infer a valid transport from: {transport}")
 
 
 
 
1
  import abc
2
  import contextlib
3
  import datetime
 
4
  import os
5
  import shutil
6
  import sys
 
7
  from collections.abc import AsyncIterator
8
  from pathlib import Path
9
  from typing import Any, TypedDict, cast
10
+ from urllib.parse import urlparse
11
 
12
  from mcp import ClientSession, StdioServerParameters
13
  from mcp.client.session import (
 
25
  from typing_extensions import Unpack
26
 
27
  from fastmcp.server import FastMCP as FastMCPServer
28
+ from fastmcp.utilities.logging import get_logger
29
+
30
+ logger = get_logger(__name__)
31
 
32
 
33
  class SessionKwargs(TypedDict, total=False):
 
488
 
489
  # the transport is a FastMCP server
490
  elif isinstance(transport, FastMCPServer):
491
+ inferred_transport = FastMCPTransport(mcp=transport)
492
 
493
  # the transport is a path to a script
494
  elif isinstance(transport, Path | str) and Path(transport).exists():
495
  if str(transport).endswith(".py"):
496
+ inferred_transport = PythonStdioTransport(script_path=transport)
497
  elif str(transport).endswith(".js"):
498
+ inferred_transport = NodeStdioTransport(script_path=transport)
499
  else:
500
  raise ValueError(f"Unsupported script type: {transport}")
501
 
502
  # the transport is an http(s) URL
503
  elif isinstance(transport, AnyUrl | str) and str(transport).startswith("http"):
504
+ transport_str = str(transport)
505
+ # Parse out just the path portion to check for /sse
506
+ parsed_url = urlparse(transport_str)
507
+ path = parsed_url.path
508
+
509
+ # Check if path contains /sse/ or ends with /sse
510
+ if "/sse/" in path or path.rstrip("/").endswith("/sse"):
511
+ inferred_transport = SSETransport(url=transport)
512
+ else:
513
+ inferred_transport = StreamableHttpTransport(url=transport)
 
 
 
 
 
 
 
514
 
515
  ## if the transport is a config dict
516
  elif isinstance(transport, dict):
 
525
  server_name = list(server.keys())[0]
526
  # Stdio transport
527
  if "command" in server[server_name] and "args" in server[server_name]:
528
+ inferred_transport = StdioTransport(
529
  command=server[server_name]["command"],
530
  args=server[server_name]["args"],
531
  env=server[server_name].get("env", None),
 
534
 
535
  # HTTP transport
536
  elif "url" in server:
537
+ inferred_transport = SSETransport(
538
  url=server["url"],
539
  headers=server.get("headers", None),
540
  )
 
544
  # the transport is an unknown type
545
  else:
546
  raise ValueError(f"Could not infer a valid transport from: {transport}")
547
+
548
+ logger.debug(f"Inferred transport: {inferred_transport}")
549
+ return inferred_transport
tests/client/test_client.py CHANGED
@@ -6,7 +6,12 @@ from mcp import McpError
6
  from pydantic import AnyUrl
7
 
8
  from fastmcp.client import Client
9
- from fastmcp.client.transports import FastMCPTransport
 
 
 
 
 
10
  from fastmcp.exceptions import ResourceError, ToolError
11
  from fastmcp.prompts.prompt import TextContent
12
  from fastmcp.server.server import FastMCP
@@ -543,3 +548,53 @@ class TestTimeout:
543
  timeout=0.01,
544
  ) as client:
545
  await client.call_tool("sleep", {"seconds": 0.1}, timeout=2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  from pydantic import AnyUrl
7
 
8
  from fastmcp.client import Client
9
+ from fastmcp.client.transports import (
10
+ FastMCPTransport,
11
+ SSETransport,
12
+ StreamableHttpTransport,
13
+ infer_transport,
14
+ )
15
  from fastmcp.exceptions import ResourceError, ToolError
16
  from fastmcp.prompts.prompt import TextContent
17
  from fastmcp.server.server import FastMCP
 
548
  timeout=0.01,
549
  ) as client:
550
  await client.call_tool("sleep", {"seconds": 0.1}, timeout=2)
551
+
552
+
553
+ class TestInferTransport:
554
+ """Tests for the infer_transport function."""
555
+
556
+ @pytest.mark.parametrize(
557
+ "url",
558
+ [
559
+ "http://example.com/api/sse/stream",
560
+ "https://localhost:8080/mcp/sse/endpoint",
561
+ "http://example.com/api/sse",
562
+ "https://localhost:8080/mcp/sse",
563
+ "http://example.com/api/sse?param=value",
564
+ "https://localhost:8080/mcp/sse/?param=value",
565
+ "https://localhost:8000/mcp/sse?x=1&y=2",
566
+ ],
567
+ ids=[
568
+ "path_with_sse_directory",
569
+ "path_with_sse_subdirectory",
570
+ "path_ending_with_sse",
571
+ "path_ending_with_sse_https",
572
+ "path_with_sse_and_query_params",
573
+ "path_with_sse_slash_and_query_params",
574
+ "path_with_sse_and_ampersand_param",
575
+ ],
576
+ )
577
+ def test_url_returns_sse_transport(self, url):
578
+ """Test that URLs with /sse/ pattern return SSETransport."""
579
+ assert isinstance(infer_transport(url), SSETransport)
580
+
581
+ @pytest.mark.parametrize(
582
+ "url",
583
+ [
584
+ "http://example.com/api",
585
+ "https://localhost:8080/mcp",
586
+ "http://example.com/asset/image.jpg",
587
+ "https://localhost:8080/sservice/endpoint",
588
+ "https://example.com/assets/file",
589
+ ],
590
+ ids=[
591
+ "regular_http_url",
592
+ "regular_https_url",
593
+ "url_with_unrelated_path",
594
+ "url_with_sservice_in_path",
595
+ "url_with_assets_in_path",
596
+ ],
597
+ )
598
+ def test_url_returns_streamable_http_transport(self, url):
599
+ """Test that URLs without /sse/ pattern return StreamableHttpTransport."""
600
+ assert isinstance(infer_transport(url), StreamableHttpTransport)