Jeremiah Lowin commited on
Commit
d2f6729
·
unverified ·
2 Parent(s): 595535426ef2ef

Merge pull request #364 from jlowin/http-pin

Browse files

Bump mcp dependency to git commit that includes streamable client

pyproject.toml CHANGED
@@ -7,7 +7,9 @@ dependencies = [
7
  "python-dotenv>=1.1.0",
8
  "exceptiongroup>=1.2.2",
9
  "httpx>=0.28.1",
10
- "mcp>=1.7.1,<2.0.0",
 
 
11
  "openapi-pydantic>=0.5.1",
12
  "rich>=13.9.4",
13
  "typer>=0.15.2",
@@ -62,6 +64,7 @@ Homepage = "https://gofastmcp.com"
62
  Repository = "https://github.com/jlowin/fastmcp"
63
  Documentation = "https://gofastmcp.com"
64
 
 
65
  [build-system]
66
  requires = ["hatchling", "uv-dynamic-versioning>=0.7.0"]
67
  build-backend = "hatchling.build"
@@ -77,6 +80,9 @@ fallback-version = "0.0.0"
77
 
78
 
79
  [tool.uv]
 
 
 
80
  # uncomment to omit `dev` default group
81
  # default-groups = []
82
 
 
7
  "python-dotenv>=1.1.0",
8
  "exceptiongroup>=1.2.2",
9
  "httpx>=0.28.1",
10
+ # "mcp>=1.7.1,<2.0.0",
11
+ # use git commit until 1.7.2 is released
12
+ "mcp",
13
  "openapi-pydantic>=0.5.1",
14
  "rich>=13.9.4",
15
  "typer>=0.15.2",
 
64
  Repository = "https://github.com/jlowin/fastmcp"
65
  Documentation = "https://gofastmcp.com"
66
 
67
+
68
  [build-system]
69
  requires = ["hatchling", "uv-dynamic-versioning>=0.7.0"]
70
  build-backend = "hatchling.build"
 
80
 
81
 
82
  [tool.uv]
83
+
84
+ [tool.uv.sources]
85
+ mcp = { git = "https://github.com/modelcontextprotocol/python-sdk", rev = "a027d75f609000378522c5873c2a16aa1963d487" }
86
  # uncomment to omit `dev` default group
87
  # default-groups = []
88
 
src/fastmcp/client/transports.py CHANGED
@@ -18,8 +18,8 @@ from mcp.client.session import (
18
  )
19
  from mcp.client.sse import sse_client
20
  from mcp.client.stdio import stdio_client
21
- from mcp.client.websocket import websocket_client
22
  from mcp.client.streamable_http import streamablehttp_client
 
23
  from mcp.shared.memory import create_connected_server_and_client_session
24
  from pydantic import AnyUrl
25
  from typing_extensions import Unpack
@@ -125,6 +125,7 @@ class SSETransport(ClientTransport):
125
  def __repr__(self) -> str:
126
  return f"<SSE(url='{self.url}')>"
127
 
 
128
  class StreamableHttpTransport(ClientTransport):
129
  """Transport implementation that connects to an MCP server via Streamable HTTP Requests."""
130
 
@@ -141,7 +142,7 @@ class StreamableHttpTransport(ClientTransport):
141
  self, **session_kwargs: Unpack[SessionKwargs]
142
  ) -> AsyncIterator[ClientSession]:
143
  async with streamablehttp_client(self.url, headers=self.headers) as transport:
144
- read_stream, write_stream = transport
145
  async with ClientSession(
146
  read_stream, write_stream, **session_kwargs
147
  ) as session:
@@ -152,7 +153,6 @@ class StreamableHttpTransport(ClientTransport):
152
  return f"<StreamableHttp(url='{self.url}')>"
153
 
154
 
155
-
156
  class StdioTransport(ClientTransport):
157
  """
158
  Base transport for connecting to an MCP server via subprocess with stdio.
 
18
  )
19
  from mcp.client.sse import sse_client
20
  from mcp.client.stdio import stdio_client
 
21
  from mcp.client.streamable_http import streamablehttp_client
22
+ from mcp.client.websocket import websocket_client
23
  from mcp.shared.memory import create_connected_server_and_client_session
24
  from pydantic import AnyUrl
25
  from typing_extensions import Unpack
 
125
  def __repr__(self) -> str:
126
  return f"<SSE(url='{self.url}')>"
127
 
128
+
129
  class StreamableHttpTransport(ClientTransport):
130
  """Transport implementation that connects to an MCP server via Streamable HTTP Requests."""
131
 
 
142
  self, **session_kwargs: Unpack[SessionKwargs]
143
  ) -> AsyncIterator[ClientSession]:
144
  async with streamablehttp_client(self.url, headers=self.headers) as transport:
145
+ read_stream, write_stream, _ = transport
146
  async with ClientSession(
147
  read_stream, write_stream, **session_kwargs
148
  ) as session:
 
153
  return f"<StreamableHttp(url='{self.url}')>"
154
 
155
 
 
156
  class StdioTransport(ClientTransport):
157
  """
158
  Base transport for connecting to an MCP server via subprocess with stdio.
tests/server/test_lifespan.py CHANGED
@@ -4,6 +4,11 @@ from collections.abc import AsyncIterator
4
  from contextlib import asynccontextmanager
5
 
6
  import anyio
 
 
 
 
 
7
  from mcp.types import (
8
  ClientCapabilities,
9
  Implementation,
@@ -14,9 +19,119 @@ from mcp.types import (
14
  )
15
  from pydantic import TypeAdapter
16
 
17
- from fastmcp import Context, FastMCP
18
 
 
 
 
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  async def test_fastmcp_server_lifespan():
21
  """Test that lifespan works in FastMCP server."""
22
 
@@ -71,41 +186,49 @@ async def test_fastmcp_server_lifespan():
71
  clientInfo=Implementation(name="test-client", version="0.1.0"),
72
  )
73
  await send_stream1.send(
74
- JSONRPCMessage(
75
- root=JSONRPCRequest(
76
- jsonrpc="2.0",
77
- id=1,
78
- method="initialize",
79
- params=TypeAdapter(InitializeRequestParams).dump_python(params),
 
 
80
  )
81
  )
82
  )
83
  response = await receive_stream2.receive()
 
84
 
85
  # Send initialized notification
86
  await send_stream1.send(
87
- JSONRPCMessage(
88
- root=JSONRPCNotification(
89
- jsonrpc="2.0",
90
- method="notifications/initialized",
 
 
91
  )
92
  )
93
  )
94
 
95
  # Call the tool to verify lifespan context
96
  await send_stream1.send(
97
- JSONRPCMessage(
98
- root=JSONRPCRequest(
99
- jsonrpc="2.0",
100
- id=2,
101
- method="tools/call",
102
- params={"name": "check_lifespan", "arguments": {}},
 
 
103
  )
104
  )
105
  )
106
 
107
  # Get response and verify
108
  response = await receive_stream2.receive()
 
109
  assert response.root.result["content"][0]["text"] == "true"
110
 
111
  # Cancel server task
 
4
  from contextlib import asynccontextmanager
5
 
6
  import anyio
7
+ import pytest
8
+ from mcp.server.fastmcp import Context, FastMCP
9
+ from mcp.server.lowlevel.server import NotificationOptions, Server
10
+ from mcp.server.models import InitializationOptions
11
+ from mcp.shared.message import SessionMessage
12
  from mcp.types import (
13
  ClientCapabilities,
14
  Implementation,
 
19
  )
20
  from pydantic import TypeAdapter
21
 
 
22
 
23
+ @pytest.mark.anyio
24
+ async def test_lowlevel_server_lifespan():
25
+ """Test that lifespan works in low-level server."""
26
 
27
+ @asynccontextmanager
28
+ async def test_lifespan(server: Server) -> AsyncIterator[dict[str, bool]]:
29
+ """Test lifespan context that tracks startup/shutdown."""
30
+ context = {"started": False, "shutdown": False}
31
+ try:
32
+ context["started"] = True
33
+ yield context
34
+ finally:
35
+ context["shutdown"] = True
36
+
37
+ server = Server("test", lifespan=test_lifespan)
38
+
39
+ # Create memory streams for testing
40
+ send_stream1, receive_stream1 = anyio.create_memory_object_stream(100)
41
+ send_stream2, receive_stream2 = anyio.create_memory_object_stream(100)
42
+
43
+ # Create a tool that accesses lifespan context
44
+ @server.call_tool()
45
+ async def check_lifespan(name: str, arguments: dict) -> list:
46
+ ctx = server.request_context
47
+ assert isinstance(ctx.lifespan_context, dict)
48
+ assert ctx.lifespan_context["started"]
49
+ assert not ctx.lifespan_context["shutdown"]
50
+ return [{"type": "text", "text": "true"}]
51
+
52
+ # Run server in background task
53
+ async with (
54
+ anyio.create_task_group() as tg,
55
+ send_stream1,
56
+ receive_stream1,
57
+ send_stream2,
58
+ receive_stream2,
59
+ ):
60
+
61
+ async def run_server():
62
+ await server.run(
63
+ receive_stream1,
64
+ send_stream2,
65
+ InitializationOptions(
66
+ server_name="test",
67
+ server_version="0.1.0",
68
+ capabilities=server.get_capabilities(
69
+ notification_options=NotificationOptions(),
70
+ experimental_capabilities={},
71
+ ),
72
+ ),
73
+ raise_exceptions=True,
74
+ )
75
+
76
+ tg.start_soon(run_server)
77
+
78
+ # Initialize the server
79
+ params = InitializeRequestParams(
80
+ protocolVersion="2024-11-05",
81
+ capabilities=ClientCapabilities(),
82
+ clientInfo=Implementation(name="test-client", version="0.1.0"),
83
+ )
84
+ await send_stream1.send(
85
+ SessionMessage(
86
+ JSONRPCMessage(
87
+ root=JSONRPCRequest(
88
+ jsonrpc="2.0",
89
+ id=1,
90
+ method="initialize",
91
+ params=TypeAdapter(InitializeRequestParams).dump_python(params),
92
+ )
93
+ )
94
+ )
95
+ )
96
+ response = await receive_stream2.receive()
97
+ response = response.message
98
+
99
+ # Send initialized notification
100
+ await send_stream1.send(
101
+ SessionMessage(
102
+ JSONRPCMessage(
103
+ root=JSONRPCNotification(
104
+ jsonrpc="2.0",
105
+ method="notifications/initialized",
106
+ )
107
+ )
108
+ )
109
+ )
110
+
111
+ # Call the tool to verify lifespan context
112
+ await send_stream1.send(
113
+ SessionMessage(
114
+ JSONRPCMessage(
115
+ root=JSONRPCRequest(
116
+ jsonrpc="2.0",
117
+ id=2,
118
+ method="tools/call",
119
+ params={"name": "check_lifespan", "arguments": {}},
120
+ )
121
+ )
122
+ )
123
+ )
124
+
125
+ # Get response and verify
126
+ response = await receive_stream2.receive()
127
+ response = response.message
128
+ assert response.root.result["content"][0]["text"] == "true"
129
+
130
+ # Cancel server task
131
+ tg.cancel_scope.cancel()
132
+
133
+
134
+ @pytest.mark.anyio
135
  async def test_fastmcp_server_lifespan():
136
  """Test that lifespan works in FastMCP server."""
137
 
 
186
  clientInfo=Implementation(name="test-client", version="0.1.0"),
187
  )
188
  await send_stream1.send(
189
+ SessionMessage(
190
+ JSONRPCMessage(
191
+ root=JSONRPCRequest(
192
+ jsonrpc="2.0",
193
+ id=1,
194
+ method="initialize",
195
+ params=TypeAdapter(InitializeRequestParams).dump_python(params),
196
+ )
197
  )
198
  )
199
  )
200
  response = await receive_stream2.receive()
201
+ response = response.message
202
 
203
  # Send initialized notification
204
  await send_stream1.send(
205
+ SessionMessage(
206
+ JSONRPCMessage(
207
+ root=JSONRPCNotification(
208
+ jsonrpc="2.0",
209
+ method="notifications/initialized",
210
+ )
211
  )
212
  )
213
  )
214
 
215
  # Call the tool to verify lifespan context
216
  await send_stream1.send(
217
+ SessionMessage(
218
+ JSONRPCMessage(
219
+ root=JSONRPCRequest(
220
+ jsonrpc="2.0",
221
+ id=2,
222
+ method="tools/call",
223
+ params={"name": "check_lifespan", "arguments": {}},
224
+ )
225
  )
226
  )
227
  )
228
 
229
  # Get response and verify
230
  response = await receive_stream2.receive()
231
+ response = response.message
232
  assert response.root.result["content"][0]["text"] == "true"
233
 
234
  # Cancel server task
uv.lock CHANGED
The diff for this file is too large to render. See raw diff