Jeremiah Lowin commited on
Commit
9a7c1b9
·
unverified ·
2 Parent(s): 29d863a977d2b6

Merge pull request #649 from jlowin/simplify-process

Browse files

Simplify code for running servers in processes during tests

src/fastmcp/utilities/tests.py CHANGED
@@ -72,14 +72,21 @@ def _run_server(mcp_server: FastMCP, transport: Literal["sse"], port: int) -> No
72
 
73
  @contextmanager
74
  def run_server_in_process(
75
- server_fn: Callable[..., None], *args
 
 
 
76
  ) -> Generator[str, None, None]:
77
  """
78
- Context manager that runs a Starlette app in a separate process and returns the
79
- server URL. When the context manager is exited, the server process is killed.
80
 
81
  Args:
82
- app: The Starlette app to run.
 
 
 
 
83
 
84
  Returns:
85
  The server URL.
@@ -87,8 +94,11 @@ def run_server_in_process(
87
  host = "127.0.0.1"
88
  port = find_available_port()
89
 
 
 
 
90
  proc = multiprocessing.Process(
91
- target=server_fn, args=(host, port, *args), daemon=True
92
  )
93
  proc.start()
94
 
 
72
 
73
  @contextmanager
74
  def run_server_in_process(
75
+ server_fn: Callable[..., None],
76
+ *args,
77
+ provide_host_and_port: bool = True,
78
+ **kwargs,
79
  ) -> Generator[str, None, None]:
80
  """
81
+ Context manager that runs a FastMCP server in a separate process and
82
+ returns the server URL. When the context manager is exited, the server process is killed.
83
 
84
  Args:
85
+ server_fn: The function that runs a FastMCP server. FastMCP servers are
86
+ not pickleable, so we need a function that creates and runs one.
87
+ *args: Arguments to pass to the server function.
88
+ provide_host_and_port: Whether to provide the host and port to the server function as kwargs.
89
+ **kwargs: Keyword arguments to pass to the server function.
90
 
91
  Returns:
92
  The server URL.
 
94
  host = "127.0.0.1"
95
  port = find_available_port()
96
 
97
+ if provide_host_and_port:
98
+ kwargs |= {"host": host, "port": port}
99
+
100
  proc = multiprocessing.Process(
101
+ target=server_fn, args=args, kwargs=kwargs, daemon=True
102
  )
103
  proc.start()
104
 
tests/auth/test_oauth_client.py CHANGED
@@ -1,11 +1,9 @@
1
- import sys
2
  from collections.abc import Generator
3
  from unittest.mock import patch
4
  from urllib.parse import parse_qs, urlparse
5
 
6
  import httpx
7
  import pytest
8
- import uvicorn
9
 
10
  import fastmcp.client.auth # Import module, not the function directly
11
  from fastmcp.client import Client
@@ -39,30 +37,13 @@ def fastmcp_server(issuer_url: str):
39
  return server
40
 
41
 
42
- def run_server(host: str, port: int, transport: str | None = None) -> None:
43
- try:
44
- # Configure OAuth provider with the actual server URL
45
- issuer_url = f"http://{host}:{port}"
46
- app = fastmcp_server(issuer_url).http_app()
47
- server = uvicorn.Server(
48
- config=uvicorn.Config(
49
- app=app,
50
- host=host,
51
- port=port,
52
- log_level="error",
53
- lifespan="on",
54
- )
55
- )
56
- server.run()
57
- except Exception as e:
58
- print(f"Server error: {e}")
59
- sys.exit(1)
60
- sys.exit(0)
61
 
62
 
63
  @pytest.fixture(scope="module")
64
  def streamable_http_server() -> Generator[str, None, None]:
65
- with run_server_in_process(run_server) as url:
66
  yield f"{url}/mcp"
67
 
68
 
 
 
1
  from collections.abc import Generator
2
  from unittest.mock import patch
3
  from urllib.parse import parse_qs, urlparse
4
 
5
  import httpx
6
  import pytest
 
7
 
8
  import fastmcp.client.auth # Import module, not the function directly
9
  from fastmcp.client import Client
 
37
  return server
38
 
39
 
40
+ def run_server(host: str, port: int, **kwargs) -> None:
41
+ fastmcp_server(f"http://{host}:{port}").run(host=host, port=port, **kwargs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
 
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
 
tests/client/test_openapi.py CHANGED
@@ -1,9 +1,7 @@
1
  import json
2
- import sys
3
  from collections.abc import Generator
4
 
5
  import pytest
6
- import uvicorn
7
  from fastapi import FastAPI, Request
8
 
9
  from fastmcp import Client, FastMCP
@@ -34,75 +32,34 @@ def fastmcp_server_for_headers() -> FastMCP:
34
  return mcp
35
 
36
 
37
- class TestClientHeaders:
38
- def run_shttp_server(self, host: str, port: int) -> None:
39
- try:
40
- app = fastmcp_server_for_headers().http_app(transport="streamable-http")
41
- server = uvicorn.Server(
42
- config=uvicorn.Config(
43
- app=app,
44
- host=host,
45
- port=port,
46
- log_level="error",
47
- lifespan="on",
48
- )
49
- )
50
- server.run()
51
- except Exception as e:
52
- print(f"Server error: {e}")
53
- sys.exit(1)
54
- sys.exit(0)
55
-
56
- def run_sse_server(self, host: str, port: int) -> None:
57
- try:
58
- app = fastmcp_server_for_headers().http_app(transport="sse")
59
- server = uvicorn.Server(
60
- config=uvicorn.Config(
61
- app=app,
62
- host=host,
63
- port=port,
64
- log_level="error",
65
- lifespan="on",
66
- )
67
- )
68
- server.run()
69
- except Exception as e:
70
- print(f"Server error: {e}")
71
- sys.exit(1)
72
- sys.exit(0)
73
-
74
- def run_proxy_server(self, host: str, port: int, remote_url: str) -> None:
75
- try:
76
- client = Client(transport=StreamableHttpTransport(remote_url))
77
- app = FastMCP.as_proxy(client).http_app(transport="streamable-http")
78
- server = uvicorn.Server(
79
- config=uvicorn.Config(
80
- app=app,
81
- host=host,
82
- port=port,
83
- log_level="error",
84
- lifespan="on",
85
- )
86
- )
87
- server.run()
88
- except Exception as e:
89
- print(f"Server error: {e}")
90
- sys.exit(1)
91
- sys.exit(0)
92
 
 
 
 
 
 
 
 
 
93
  @pytest.fixture(scope="class")
94
  def shttp_server(self) -> Generator[str, None, None]:
95
- with run_server_in_process(self.run_shttp_server) as url:
96
  yield f"{url}/mcp"
97
 
98
  @pytest.fixture(scope="class")
99
  def sse_server(self) -> Generator[str, None, None]:
100
- with run_server_in_process(self.run_sse_server) as url:
101
  yield f"{url}/sse"
102
 
103
  @pytest.fixture(scope="class")
104
  def proxy_server(self, shttp_server: str) -> Generator[str, None, None]:
105
- with run_server_in_process(self.run_proxy_server, shttp_server + "/mcp") as url:
 
 
 
 
106
  yield f"{url}/mcp"
107
 
108
  async def test_client_headers_sse_resource(self, sse_server: str):
 
1
  import json
 
2
  from collections.abc import Generator
3
 
4
  import pytest
 
5
  from fastapi import FastAPI, Request
6
 
7
  from fastmcp import Client, FastMCP
 
32
  return mcp
33
 
34
 
35
+ def run_server(host: str, port: int, **kwargs) -> None:
36
+ fastmcp_server_for_headers().run(host=host, port=port, **kwargs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
+
39
+ def run_proxy_server(host: str, port: int, shttp_url: str, **kwargs) -> None:
40
+ client = Client(transport=StreamableHttpTransport(shttp_url))
41
+ app = FastMCP.as_proxy(client)
42
+ app.run(host=host, port=port, **kwargs)
43
+
44
+
45
+ class TestClientHeaders:
46
  @pytest.fixture(scope="class")
47
  def shttp_server(self) -> Generator[str, None, None]:
48
+ with run_server_in_process(run_server, transport="streamable-http") as url:
49
  yield f"{url}/mcp"
50
 
51
  @pytest.fixture(scope="class")
52
  def sse_server(self) -> Generator[str, None, None]:
53
+ with run_server_in_process(run_server, transport="sse") as url:
54
  yield f"{url}/sse"
55
 
56
  @pytest.fixture(scope="class")
57
  def proxy_server(self, shttp_server: str) -> Generator[str, None, None]:
58
+ with run_server_in_process(
59
+ run_proxy_server,
60
+ shttp_url=shttp_server,
61
+ transport="streamable-http",
62
+ ) as url:
63
  yield f"{url}/mcp"
64
 
65
  async def test_client_headers_sse_resource(self, sse_server: str):
tests/client/test_sse.py CHANGED
@@ -63,22 +63,13 @@ def fastmcp_server():
63
  return server
64
 
65
 
66
- def run_server(host: str, port: int, path: str | None = None) -> None:
67
- try:
68
- app = fastmcp_server().http_app(transport="sse", path=path)
69
- server = uvicorn.Server(
70
- config=uvicorn.Config(app=app, host=host, port=port, log_level="error")
71
- )
72
- server.run()
73
- except Exception as e:
74
- print(f"Server error: {e}")
75
- sys.exit(1)
76
- sys.exit(0)
77
 
78
 
79
  @pytest.fixture(autouse=True, scope="module")
80
  def sse_server() -> Generator[str, None, None]:
81
- with run_server_in_process(run_server) as url:
82
  yield f"{url}/sse"
83
 
84
 
@@ -101,22 +92,17 @@ async def test_http_headers(sse_server: str):
101
 
102
 
103
  def run_nested_server(host: str, port: int) -> None:
104
- try:
105
- app = fastmcp_server().sse_app(path="/mcp/sse", message_path="/mcp/messages")
106
- mount = Starlette(routes=[Mount("/nest-inner", app=app)])
107
- mount2 = Starlette(routes=[Mount("/nest-outer", app=mount)])
108
- server = uvicorn.Server(
109
- config=uvicorn.Config(app=mount2, host=host, port=port, log_level="error")
110
- )
111
- server.run()
112
- except Exception as e:
113
- print(f"Server error: {e}")
114
- sys.exit(1)
115
- sys.exit(0)
116
 
117
 
118
  async def test_run_server_on_path():
119
- with run_server_in_process(run_server, "/help") as url:
120
  async with Client(transport=SSETransport(f"{url}/help")) as client:
121
  result = await client.ping()
122
  assert result is True
 
63
  return server
64
 
65
 
66
+ def run_server(host: str, port: int, **kwargs) -> None:
67
+ fastmcp_server().run(host=host, port=port, **kwargs)
 
 
 
 
 
 
 
 
 
68
 
69
 
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
 
 
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(
99
+ config=uvicorn.Config(app=mount2, host=host, port=port, log_level="error")
100
+ )
101
+ server.run()
 
 
 
 
 
102
 
103
 
104
  async def test_run_server_on_path():
105
+ with run_server_in_process(run_server, transport="sse", path="/help") as url:
106
  async with Client(transport=SSETransport(f"{url}/help")) as client:
107
  result = await client.ping()
108
  assert result is True
tests/client/test_streamable_http.py CHANGED
@@ -63,28 +63,33 @@ def fastmcp_server():
63
  return server
64
 
65
 
66
- def run_server(host: str, port: int) -> None:
67
- try:
68
- app = fastmcp_server().http_app()
69
- server = uvicorn.Server(
70
- config=uvicorn.Config(
71
- app=app,
72
- host=host,
73
- port=port,
74
- log_level="error",
75
- lifespan="on",
76
- )
 
 
 
 
 
 
 
 
77
  )
78
- server.run()
79
- except Exception as e:
80
- print(f"Server error: {e}")
81
- sys.exit(1)
82
- sys.exit(0)
83
 
84
 
85
  @pytest.fixture(scope="module")
86
  def streamable_http_server() -> Generator[str, None, None]:
87
- with run_server_in_process(run_server) as url:
88
  yield f"{url}/mcp"
89
 
90
 
@@ -110,31 +115,6 @@ async def test_http_headers(streamable_http_server: str):
110
  assert json_result["x-demo-header"] == "ABC"
111
 
112
 
113
- def run_nested_server(host: str, port: int) -> None:
114
- try:
115
- mcp_app = fastmcp_server().http_app(path="/final/mcp")
116
-
117
- mount = Starlette(routes=[Mount("/nest-inner", app=mcp_app)])
118
- mount2 = Starlette(
119
- routes=[Mount("/nest-outer", app=mount)],
120
- lifespan=mcp_app.lifespan,
121
- )
122
- server = uvicorn.Server(
123
- config=uvicorn.Config(
124
- app=mount2,
125
- host=host,
126
- port=port,
127
- log_level="error",
128
- lifespan="on",
129
- )
130
- )
131
- server.run()
132
- except Exception as e:
133
- print(f"Server error: {e}")
134
- sys.exit(1)
135
- sys.exit(0)
136
-
137
-
138
  async def test_nested_streamable_http_server_resolves_correctly():
139
  # tests patch for
140
  # https://github.com/modelcontextprotocol/python-sdk/pull/659
 
63
  return server
64
 
65
 
66
+ def run_server(host: str, port: int, **kwargs) -> None:
67
+ fastmcp_server().run(host=host, port=port, **kwargs)
68
+
69
+
70
+ def run_nested_server(host: str, port: int) -> None:
71
+ mcp_app = fastmcp_server().http_app(path="/final/mcp")
72
+
73
+ mount = Starlette(routes=[Mount("/nest-inner", app=mcp_app)])
74
+ mount2 = Starlette(
75
+ routes=[Mount("/nest-outer", app=mount)],
76
+ lifespan=mcp_app.lifespan,
77
+ )
78
+ server = uvicorn.Server(
79
+ config=uvicorn.Config(
80
+ app=mount2,
81
+ host=host,
82
+ port=port,
83
+ log_level="error",
84
+ lifespan="on",
85
  )
86
+ )
87
+ server.run()
 
 
 
88
 
89
 
90
  @pytest.fixture(scope="module")
91
  def streamable_http_server() -> Generator[str, None, None]:
92
+ with run_server_in_process(run_server, transport="streamable-http") as url:
93
  yield f"{url}/mcp"
94
 
95
 
 
115
  assert json_result["x-demo-header"] == "ABC"
116
 
117
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  async def test_nested_streamable_http_server_resolves_correctly():
119
  # tests patch for
120
  # https://github.com/modelcontextprotocol/python-sdk/pull/659
tests/server/http/test_http_dependencies.py CHANGED
@@ -1,9 +1,7 @@
1
  import json
2
- import sys
3
  from collections.abc import Generator
4
 
5
  import pytest
6
- import uvicorn
7
 
8
  from fastmcp.client import Client
9
  from fastmcp.client.transports import SSETransport, StreamableHttpTransport
@@ -40,53 +38,19 @@ def fastmcp_server():
40
  return server
41
 
42
 
43
- def run_shttp_server(host: str, port: int) -> None:
44
- try:
45
- app = fastmcp_server().http_app(transport="streamable-http")
46
- server = uvicorn.Server(
47
- config=uvicorn.Config(
48
- app=app,
49
- host=host,
50
- port=port,
51
- log_level="error",
52
- lifespan="on",
53
- )
54
- )
55
- server.run()
56
- except Exception as e:
57
- print(f"Server error: {e}")
58
- sys.exit(1)
59
- sys.exit(0)
60
-
61
-
62
- def run_sse_server(host: str, port: int) -> None:
63
- try:
64
- app = fastmcp_server().http_app(transport="sse")
65
- server = uvicorn.Server(
66
- config=uvicorn.Config(
67
- app=app,
68
- host=host,
69
- port=port,
70
- log_level="error",
71
- lifespan="on",
72
- )
73
- )
74
- server.run()
75
- except Exception as e:
76
- print(f"Server error: {e}")
77
- sys.exit(1)
78
- sys.exit(0)
79
 
80
 
81
  @pytest.fixture(autouse=True, scope="module")
82
  def shttp_server() -> Generator[str, None, None]:
83
- with run_server_in_process(run_shttp_server) as url:
84
  yield f"{url}/mcp"
85
 
86
 
87
  @pytest.fixture(autouse=True, scope="module")
88
  def sse_server() -> Generator[str, None, None]:
89
- with run_server_in_process(run_sse_server) as url:
90
  yield f"{url}/sse"
91
 
92
 
 
1
  import json
 
2
  from collections.abc import Generator
3
 
4
  import pytest
 
5
 
6
  from fastmcp.client import Client
7
  from fastmcp.client.transports import SSETransport, StreamableHttpTransport
 
38
  return server
39
 
40
 
41
+ def run_server(host: str, port: int, **kwargs) -> None:
42
+ fastmcp_server().run(host=host, port=port, **kwargs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
 
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