Jeremiah Lowin commited on
Commit
3acddf3
·
1 Parent(s): 59c76e7

Reorganize clients and transports

Browse files
examples/modular_app.py DELETED
@@ -1,150 +0,0 @@
1
- """
2
- Modular FastMCP Application Example
3
-
4
- This example demonstrates building a modular application with FastMCP
5
- by separating functionality into domain-specific modules.
6
- """
7
-
8
- import asyncio
9
- from pathlib import Path
10
- from typing import Any, Dict, List
11
-
12
- from fastmcp import Context, FastMCP
13
-
14
- # ----- DATA MODULE -----
15
- data_app = FastMCP("Data Module")
16
-
17
- # Simulated database
18
- users_db = [
19
- {"id": 1, "name": "Alice", "email": "alice@example.com"},
20
- {"id": 2, "name": "Bob", "email": "bob@example.com"},
21
- {"id": 3, "name": "Charlie", "email": "charlie@example.com"},
22
- ]
23
-
24
-
25
- @data_app.resource("users://all")
26
- def get_all_users() -> List[Dict[str, Any]]:
27
- """Get all users in the database"""
28
- return users_db
29
-
30
-
31
- @data_app.resource("users://{user_id}")
32
- def get_user_by_id(user_id: str) -> dict[str, Any] | None:
33
- """Get a specific user by ID"""
34
- user_id_int = int(user_id)
35
- for user in users_db:
36
- if user["id"] == user_id_int:
37
- return user
38
- return None
39
-
40
-
41
- @data_app.tool()
42
- async def create_user(name: str, email: str, ctx: Context) -> Dict[str, Any]:
43
- """Add a new user to the database"""
44
- # Simulate a slow operation
45
- await ctx.info(f"Creating user {name}...")
46
- await asyncio.sleep(1)
47
-
48
- # Create user
49
- new_id = max(user["id"] for user in users_db) + 1
50
- new_user = {"id": new_id, "name": name, "email": email}
51
- users_db.append(new_user)
52
-
53
- await ctx.info(f"User created with ID {new_id}")
54
- return new_user
55
-
56
-
57
- # ----- ANALYTICS MODULE -----
58
- analytics_app = FastMCP("Analytics Module")
59
-
60
-
61
- @analytics_app.tool()
62
- async def analyze_users(ctx: Context) -> Dict[str, Any]:
63
- """Run analytics on user data"""
64
- # Get user data from the data module
65
- users = await ctx.read_resource("data:users://all")
66
-
67
- # Perform analytics
68
- await ctx.info("Analyzing user data...")
69
- await asyncio.sleep(1)
70
-
71
- # Return analytics results
72
- return {
73
- "total_users": len(users),
74
- "domains": {user["email"].split("@")[1] for user in users},
75
- }
76
-
77
-
78
- @analytics_app.resource("analytics://summary")
79
- def get_analytics_summary() -> Dict[str, Any]:
80
- """Get a summary of analytics data"""
81
- return {"active_users": len(users_db), "last_updated": "2023-06-01"}
82
-
83
-
84
- # ----- FILESYSTEM MODULE -----
85
- files_app = FastMCP("Filesystem Module")
86
-
87
-
88
- @files_app.resource("files://desktop")
89
- def list_desktop_files() -> List[str]:
90
- """List files on the user's desktop"""
91
- desktop = Path.home() / "Desktop"
92
- return [f.name for f in desktop.iterdir() if f.is_file()]
93
-
94
-
95
- @files_app.tool()
96
- async def search_files(query: str, ctx: Context) -> List[str]:
97
- """Search for files matching a query"""
98
- await ctx.info(f"Searching for files matching '{query}'...")
99
-
100
- # Simulate a file search
101
- desktop = Path.home() / "Desktop"
102
- files = [
103
- f.name
104
- for f in desktop.iterdir()
105
- if f.is_file() and query.lower() in f.name.lower()
106
- ]
107
-
108
- await ctx.info(f"Found {len(files)} matching files")
109
- return files
110
-
111
-
112
- # ----- MAIN APPLICATION -----
113
- # Create the main application that combines all modules
114
- main_app = FastMCP("Modular FastMCP Demo")
115
-
116
-
117
- @main_app.tool()
118
- async def get_system_info(ctx: Context) -> Dict[str, Any]:
119
- """Get comprehensive system information"""
120
- await ctx.info("Gathering system information...")
121
-
122
- # Use the mounted modules to gather info
123
- users = await ctx.read_resource("data:users://all")
124
- analytics = await ctx.read_resource("analytics:analytics://summary")
125
- desktop_files = await ctx.read_resource("files:files://desktop")
126
-
127
- return {
128
- "users": {"count": len(users), "names": [user["name"] for user in users]},
129
- "analytics": analytics,
130
- "files": {"desktop_count": len(desktop_files)},
131
- }
132
-
133
-
134
- # Mount all modules to the main app
135
- main_app.mount("data", data_app)
136
- main_app.mount("analytics", analytics_app)
137
- main_app.mount("files", files_app)
138
-
139
- if __name__ == "__main__":
140
- # Now register resources (which requires async)
141
- async def initialize_resources():
142
- await main_app.register_all_mounted_resources()
143
- print("Resources registered successfully!")
144
-
145
- # Initialize resources
146
- asyncio.run(initialize_resources())
147
-
148
- # Start the server
149
- print("Starting modular FastMCP application...")
150
- main_app.run()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/fastmcp/__init__.py CHANGED
@@ -1,11 +1,12 @@
1
  """FastMCP - An ergonomic MCP interface."""
2
 
3
  from importlib.metadata import version
4
- import fastmcp.settings
5
 
6
  from fastmcp.server.server import FastMCP
7
  from fastmcp.server.context import Context
8
- from . import clients
 
9
 
10
  __version__ = version("fastmcp")
11
- __all__ = ["FastMCP", "Context", "clients"]
 
1
  """FastMCP - An ergonomic MCP interface."""
2
 
3
  from importlib.metadata import version
4
+
5
 
6
  from fastmcp.server.server import FastMCP
7
  from fastmcp.server.context import Context
8
+ from fastmcp.client import Client
9
+ from . import client, settings
10
 
11
  __version__ = version("fastmcp")
12
+ __all__ = ["FastMCP", "Context", "client", "settings"]
src/fastmcp/client/__init__.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .client import Client
2
+ from .transports import (
3
+ ClientTransport,
4
+ WSTransport,
5
+ SSETransport,
6
+ StdioTransport,
7
+ PythonStdioTransport,
8
+ NodeStdioTransport,
9
+ UvxStdioTransport,
10
+ NpxStdioTransport,
11
+ FastMCPTransport,
12
+ )
13
+
14
+ __all__ = [
15
+ "Client",
16
+ "ClientTransport",
17
+ "WSTransport",
18
+ "SSETransport",
19
+ "StdioTransport",
20
+ "PythonStdioTransport",
21
+ "NodeStdioTransport",
22
+ "UvxStdioTransport",
23
+ "NpxStdioTransport",
24
+ "FastMCPTransport",
25
+ ]
src/fastmcp/client/base.py ADDED
@@ -0,0 +1 @@
 
 
1
+
src/fastmcp/{clients/base.py → client/client.py} RENAMED
@@ -1,14 +1,22 @@
1
- import abc
2
- import contextlib
3
  import datetime
4
- from typing import Any, AsyncContextManager, TypedDict
 
5
 
6
  import mcp.types
7
  from mcp import ClientSession
8
- from mcp.client.session import ListRootsFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT
 
 
 
 
 
9
  from mcp.shared.context import LifespanContextT, RequestContext
10
  from pydantic import AnyUrl
11
 
 
 
 
 
12
 
13
  def _get_roots_callback(roots: list[mcp.types.Root]) -> ListRootsFnT | None:
14
  async def _roots_callback(
@@ -19,26 +27,18 @@ def _get_roots_callback(roots: list[mcp.types.Root]) -> ListRootsFnT | None:
19
  return _roots_callback
20
 
21
 
22
- class ClientKwargs(TypedDict, total=False):
23
- roots: list[mcp.types.Root] | None
24
- sampling_callback: SamplingFnT | None
25
- list_roots_callback: ListRootsFnT | None
26
- logging_callback: LoggingFnT | None
27
- message_handler: MessageHandlerFnT | None
28
- read_timeout_seconds: datetime.timedelta | None
29
-
30
-
31
- class SessionKwargs(TypedDict, total=False):
32
- sampling_callback: SamplingFnT | None
33
- list_roots_callback: ListRootsFnT | None
34
- logging_callback: LoggingFnT | None
35
- message_handler: MessageHandlerFnT | None
36
- read_timeout_seconds: datetime.timedelta | None
37
 
 
 
 
38
 
39
- class BaseClient(abc.ABC):
40
  def __init__(
41
  self,
 
 
42
  roots: list[mcp.types.Root] | None = None,
43
  sampling_callback: SamplingFnT | None = None,
44
  list_roots_callback: ListRootsFnT | None = None,
@@ -46,109 +46,69 @@ class BaseClient(abc.ABC):
46
  message_handler: MessageHandlerFnT | None = None,
47
  read_timeout_seconds: datetime.timedelta | None = None,
48
  ):
49
- self._transport: Any | None = None
50
  self._session: ClientSession | None = None
51
- self._cm: AsyncContextManager | None = None
52
-
53
- if roots is not None:
54
- if list_roots_callback is not None:
55
- raise ValueError(
56
- "Cannot provide both `roots` and `list_roots_callback`. "
57
- "Either provide a list of roots or a callback to list roots."
58
- )
59
- else:
60
- list_roots_callback = _get_roots_callback(roots)
61
-
62
- self._sampling_callback = sampling_callback
63
- self._list_roots_callback = list_roots_callback
64
- self._logging_callback = logging_callback
65
- self._message_handler = message_handler
66
- self._read_timeout_seconds = read_timeout_seconds
67
-
68
- def _session_kwargs(self) -> SessionKwargs:
69
- return SessionKwargs(
70
- sampling_callback=self._sampling_callback,
71
- list_roots_callback=self._list_roots_callback,
72
- logging_callback=self._logging_callback,
73
- message_handler=self._message_handler,
74
- read_timeout_seconds=self._read_timeout_seconds,
75
  )
76
 
77
- @property
78
- def transport(self):
79
- """Get the current transport connection"""
80
- if self._transport is None:
81
- raise RuntimeError(
82
- "Client is not connected. Use 'async with client:' context manager first."
83
- )
84
- return self._transport
85
 
86
  @property
87
- def session(self):
88
- """Get the current session"""
89
  if self._session is None:
90
  raise RuntimeError(
91
  "Client is not connected. Use 'async with client:' context manager first."
92
  )
93
  return self._session
94
 
95
- def is_connected(self):
96
- """Check if the client is currently connected"""
97
  return self._session is not None
98
 
99
- @abc.abstractmethod
100
- def _connect(self) -> AsyncContextManager:
101
- """Return an async context manager that handles connection lifecycle.
102
- This will be called by __aenter__ to establish the connection."""
103
- raise NotImplementedError("Subclasses must implement this method")
104
-
105
- @contextlib.asynccontextmanager
106
- async def _create_connection_context(self):
107
- """Create and manage the connection context if not already connected.
108
- This handles both creating a new connection or reusing an existing one."""
109
- created_connection = False
110
- try:
111
- if not self.is_connected():
112
- # Only create a new connection if not already connected
113
- self._cm = self._connect()
114
- await self._cm.__aenter__()
115
- created_connection = True
116
- yield
117
- finally:
118
- if created_connection and self._cm is not None:
119
- # Only close if we created the connection in this context
120
- await self._cm.__aexit__(None, None, None)
121
- self._transport = None
122
- self._session = None
123
- self._cm = None
124
-
125
- @contextlib.asynccontextmanager
126
- async def _set_session(self, transport: Any, session: ClientSession):
127
- self._transport = transport
128
- self._session = session
129
  try:
130
- await self._session.initialize()
131
- yield
132
- finally:
133
- self._transport = None
 
134
  self._session = None
135
-
136
- async def __aenter__(self):
137
- self._connection_ctx = self._create_connection_context()
138
- await self._connection_ctx.__aenter__()
139
- return self
140
 
141
  async def __aexit__(self, exc_type, exc_val, exc_tb):
142
- await self._connection_ctx.__aexit__(exc_type, exc_val, exc_tb)
 
 
 
143
 
144
  # --- MCP Client Methods ---
145
-
146
  async def ping(self) -> None:
147
  """Send a ping request."""
148
  await self.session.send_ping()
149
 
150
  async def progress(
151
- self, progress_token: str | int, progress: float, total: float | None = None
 
 
 
152
  ) -> None:
153
  """Send a progress notification."""
154
  await self.session.send_progress_notification(progress_token, progress, total)
@@ -168,7 +128,7 @@ class BaseClient(abc.ABC):
168
  async def read_resource(self, uri: AnyUrl | str) -> mcp.types.ReadResourceResult:
169
  """Send a resources/read request."""
170
  if isinstance(uri, str):
171
- uri = AnyUrl(uri)
172
  return await self.session.read_resource(uri)
173
 
174
  async def subscribe_resource(self, uri: AnyUrl | str) -> None:
 
 
 
1
  import datetime
2
+ from pathlib import Path
3
+ from typing import Any, AsyncContextManager
4
 
5
  import mcp.types
6
  from mcp import ClientSession
7
+ from mcp.client.session import (
8
+ ListRootsFnT,
9
+ LoggingFnT,
10
+ MessageHandlerFnT,
11
+ SamplingFnT,
12
+ )
13
  from mcp.shared.context import LifespanContextT, RequestContext
14
  from pydantic import AnyUrl
15
 
16
+ from fastmcp.server import FastMCP
17
+
18
+ from .transports import ClientTransport, SessionKwargs, infer_transport
19
+
20
 
21
  def _get_roots_callback(roots: list[mcp.types.Root]) -> ListRootsFnT | None:
22
  async def _roots_callback(
 
27
  return _roots_callback
28
 
29
 
30
+ class Client:
31
+ """
32
+ MCP client that delegates connection management to a Transport instance.
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
+ The Client class is primarily concerned with MCP protocol logic,
35
+ while the Transport handles connection establishment and management.
36
+ """
37
 
 
38
  def __init__(
39
  self,
40
+ transport: ClientTransport | FastMCP | AnyUrl | Path | str,
41
+ # Common args
42
  roots: list[mcp.types.Root] | None = None,
43
  sampling_callback: SamplingFnT | None = None,
44
  list_roots_callback: ListRootsFnT | None = None,
 
46
  message_handler: MessageHandlerFnT | None = None,
47
  read_timeout_seconds: datetime.timedelta | None = None,
48
  ):
49
+ self.transport = infer_transport(transport)
50
  self._session: ClientSession | None = None
51
+ self._session_cm: AsyncContextManager[ClientSession] | None = None
52
+
53
+ # Store common kwargs to pass to transport.connect_session
54
+ if roots is not None and list_roots_callback is not None:
55
+ raise ValueError("Cannot provide both `roots` and `list_roots_callback`.")
56
+ resolved_list_roots_callback = list_roots_callback or (
57
+ _get_roots_callback(roots) if roots else None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  )
59
 
60
+ self._session_kwargs: SessionKwargs = {
61
+ "sampling_callback": sampling_callback,
62
+ "list_roots_callback": resolved_list_roots_callback,
63
+ "logging_callback": logging_callback,
64
+ "message_handler": message_handler,
65
+ "read_timeout_seconds": read_timeout_seconds,
66
+ }
 
67
 
68
  @property
69
+ def session(self) -> ClientSession:
70
+ """Get the current active session. Raises RuntimeError if not connected."""
71
  if self._session is None:
72
  raise RuntimeError(
73
  "Client is not connected. Use 'async with client:' context manager first."
74
  )
75
  return self._session
76
 
77
+ def is_connected(self) -> bool:
78
+ """Check if the client is currently connected."""
79
  return self._session is not None
80
 
81
+ async def __aenter__(self):
82
+ if self.is_connected():
83
+ raise RuntimeError("Client is already connected in an async context.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  try:
85
+ self._session_cm = self.transport.connect_session(**self._session_kwargs)
86
+ self._session = await self._session_cm.__aenter__()
87
+ return self
88
+ except Exception as e:
89
+ # Ensure cleanup if __aenter__ fails partially
90
  self._session = None
91
+ self._session_cm = None
92
+ raise ConnectionError(
93
+ f"Failed to connect using {self.transport}: {e}"
94
+ ) from e
 
95
 
96
  async def __aexit__(self, exc_type, exc_val, exc_tb):
97
+ if self._session_cm:
98
+ await self._session_cm.__aexit__(exc_type, exc_val, exc_tb)
99
+ self._session = None
100
+ self._session_cm = None
101
 
102
  # --- MCP Client Methods ---
 
103
  async def ping(self) -> None:
104
  """Send a ping request."""
105
  await self.session.send_ping()
106
 
107
  async def progress(
108
+ self,
109
+ progress_token: str | int,
110
+ progress: float,
111
+ total: float | None = None,
112
  ) -> None:
113
  """Send a progress notification."""
114
  await self.session.send_progress_notification(progress_token, progress, total)
 
128
  async def read_resource(self, uri: AnyUrl | str) -> mcp.types.ReadResourceResult:
129
  """Send a resources/read request."""
130
  if isinstance(uri, str):
131
+ uri = AnyUrl(uri) # Ensure AnyUrl
132
  return await self.session.read_resource(uri)
133
 
134
  async def subscribe_resource(self, uri: AnyUrl | str) -> None:
src/fastmcp/client/transports.py ADDED
@@ -0,0 +1,415 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import abc
2
+ import contextlib
3
+ import datetime
4
+ import os
5
+ from pathlib import Path
6
+ from typing import (
7
+ AsyncIterator,
8
+ Dict,
9
+ List,
10
+ Optional,
11
+ TypedDict,
12
+ Union,
13
+ )
14
+
15
+ from mcp import ClientSession, StdioServerParameters
16
+ from mcp.client.session import (
17
+ ListRootsFnT,
18
+ LoggingFnT,
19
+ MessageHandlerFnT,
20
+ SamplingFnT,
21
+ )
22
+ from mcp.client.sse import sse_client
23
+ from mcp.client.stdio import stdio_client
24
+ from mcp.client.websocket import websocket_client
25
+ from mcp.shared.memory import create_connected_server_and_client_session
26
+ from pydantic import AnyUrl
27
+ from typing_extensions import Unpack
28
+
29
+ from fastmcp.server import FastMCP as FastMCPServer
30
+
31
+
32
+ class SessionKwargs(TypedDict, total=False):
33
+ """Keyword arguments for the MCP ClientSession constructor."""
34
+
35
+ sampling_callback: SamplingFnT | None
36
+ list_roots_callback: ListRootsFnT | None
37
+ logging_callback: LoggingFnT | None
38
+ message_handler: MessageHandlerFnT | None
39
+ read_timeout_seconds: datetime.timedelta | None
40
+
41
+
42
+ class ClientTransport(abc.ABC):
43
+ """
44
+ Abstract base class for different MCP client transport mechanisms.
45
+
46
+ A Transport is responsible for establishing and managing connections
47
+ to an MCP server, and providing a ClientSession within an async context.
48
+ """
49
+
50
+ @abc.abstractmethod
51
+ @contextlib.asynccontextmanager
52
+ async def connect_session(
53
+ self, **session_kwargs: Unpack[SessionKwargs]
54
+ ) -> AsyncIterator[ClientSession]:
55
+ """
56
+ Establishes a connection and yields an active, initialized ClientSession.
57
+
58
+ The session is guaranteed to be valid only within the scope of the
59
+ async context manager. Connection setup and teardown are handled
60
+ within this context.
61
+
62
+ Args:
63
+ **session_kwargs: Keyword arguments to pass to the ClientSession
64
+ constructor (e.g., callbacks, timeouts).
65
+
66
+ Yields:
67
+ An initialized mcp.ClientSession instance.
68
+ """
69
+ raise NotImplementedError
70
+ yield None # type: ignore
71
+
72
+ def __repr__(self) -> str:
73
+ # Basic representation for subclasses
74
+ return f"<{self.__class__.__name__}>"
75
+
76
+
77
+ class WSTransport(ClientTransport):
78
+ """Transport implementation that connects to an MCP server via WebSockets."""
79
+
80
+ def __init__(self, url: str | AnyUrl):
81
+ if isinstance(url, AnyUrl):
82
+ url = str(url)
83
+ if not isinstance(url, str) or not url.startswith("ws"):
84
+ raise ValueError("Invalid WebSocket URL provided.")
85
+ self.url = url
86
+
87
+ @contextlib.asynccontextmanager
88
+ async def connect_session(
89
+ self, **session_kwargs: Unpack[SessionKwargs]
90
+ ) -> AsyncIterator[ClientSession]:
91
+ async with websocket_client(self.url) as transport:
92
+ read_stream, write_stream = transport
93
+ async with ClientSession(
94
+ read_stream, write_stream, **session_kwargs
95
+ ) as session:
96
+ await session.initialize() # Initialize after session creation
97
+ yield session
98
+
99
+ def __repr__(self) -> str:
100
+ return f"<WebSocket(url='{self.url}')>"
101
+
102
+
103
+ class SSETransport(ClientTransport):
104
+ """Transport implementation that connects to an MCP server via Server-Sent Events."""
105
+
106
+ def __init__(self, url: str | AnyUrl, headers: Optional[Dict[str, str]] = None):
107
+ if isinstance(url, AnyUrl):
108
+ url = str(url)
109
+ if not isinstance(url, str) or not url.startswith("http"):
110
+ raise ValueError("Invalid HTTP/S URL provided for SSE.")
111
+ self.url = url
112
+ self.headers = headers or {}
113
+
114
+ @contextlib.asynccontextmanager
115
+ async def connect_session(
116
+ self, **session_kwargs: Unpack[SessionKwargs]
117
+ ) -> AsyncIterator[ClientSession]:
118
+ async with sse_client(self.url, headers=self.headers) as transport:
119
+ read_stream, write_stream = transport
120
+ async with ClientSession(
121
+ read_stream, write_stream, **session_kwargs
122
+ ) as session:
123
+ await session.initialize()
124
+ yield session
125
+
126
+ def __repr__(self) -> str:
127
+ return f"<SSE(url='{self.url}')>"
128
+
129
+
130
+ class StdioTransport(ClientTransport):
131
+ """
132
+ Base transport for connecting to an MCP server via subprocess with stdio.
133
+
134
+ This is a base class that can be subclassed for specific command-based
135
+ transports like Python, Node, Uvx, etc.
136
+ """
137
+
138
+ def __init__(
139
+ self,
140
+ command: str,
141
+ args: List[str],
142
+ env: Optional[Dict[str, str]] = None,
143
+ cwd: Optional[str] = None,
144
+ ):
145
+ """
146
+ Initialize a Stdio transport.
147
+
148
+ Args:
149
+ command: The command to run (e.g., "python", "node", "uvx")
150
+ args: The arguments to pass to the command
151
+ env: Environment variables to set for the subprocess
152
+ cwd: Current working directory for the subprocess
153
+ """
154
+ self.command = command
155
+ self.args = args
156
+ self.env = env
157
+ self.cwd = cwd
158
+
159
+ @contextlib.asynccontextmanager
160
+ async def connect_session(
161
+ self, **session_kwargs: Unpack[SessionKwargs]
162
+ ) -> AsyncIterator[ClientSession]:
163
+ server_params = StdioServerParameters(
164
+ command=self.command, args=self.args, env=self.env, cwd=self.cwd
165
+ )
166
+ async with stdio_client(server_params) as transport:
167
+ read_stream, write_stream = transport
168
+ async with ClientSession(
169
+ read_stream, write_stream, **session_kwargs
170
+ ) as session:
171
+ await session.initialize()
172
+ yield session
173
+
174
+ def __repr__(self) -> str:
175
+ return (
176
+ f"<{self.__class__.__name__}(command='{self.command}', args={self.args})>"
177
+ )
178
+
179
+
180
+ class PythonStdioTransport(StdioTransport):
181
+ """Transport for running Python scripts."""
182
+
183
+ def __init__(
184
+ self,
185
+ script_path: Union[str, Path],
186
+ args: Optional[List[str]] = None,
187
+ env: Optional[Dict[str, str]] = None,
188
+ cwd: Optional[str] = None,
189
+ python_cmd: str = "python",
190
+ ):
191
+ """
192
+ Initialize a Python transport.
193
+
194
+ Args:
195
+ script_path: Path to the Python script to run
196
+ args: Additional arguments to pass to the script
197
+ env: Environment variables to set for the subprocess
198
+ cwd: Current working directory for the subprocess
199
+ python_cmd: Python command to use (default: "python")
200
+ """
201
+ script_path = Path(script_path).resolve()
202
+ if not script_path.is_file():
203
+ raise FileNotFoundError(f"Script not found: {script_path}")
204
+ if not str(script_path).endswith(".py"):
205
+ raise ValueError(f"Not a Python script: {script_path}")
206
+
207
+ full_args = [str(script_path)]
208
+ if args:
209
+ full_args.extend(args)
210
+
211
+ super().__init__(command=python_cmd, args=full_args, env=env, cwd=cwd)
212
+ self.script_path = script_path
213
+
214
+
215
+ class NodeStdioTransport(StdioTransport):
216
+ """Transport for running Node.js scripts."""
217
+
218
+ def __init__(
219
+ self,
220
+ script_path: Union[str, Path],
221
+ args: Optional[List[str]] = None,
222
+ env: Optional[Dict[str, str]] = None,
223
+ cwd: Optional[str] = None,
224
+ node_cmd: str = "node",
225
+ ):
226
+ """
227
+ Initialize a Node transport.
228
+
229
+ Args:
230
+ script_path: Path to the Node.js script to run
231
+ args: Additional arguments to pass to the script
232
+ env: Environment variables to set for the subprocess
233
+ cwd: Current working directory for the subprocess
234
+ node_cmd: Node.js command to use (default: "node")
235
+ """
236
+ script_path = Path(script_path).resolve()
237
+ if not script_path.is_file():
238
+ raise FileNotFoundError(f"Script not found: {script_path}")
239
+ if not str(script_path).endswith(".js"):
240
+ raise ValueError(f"Not a JavaScript script: {script_path}")
241
+
242
+ full_args = [str(script_path)]
243
+ if args:
244
+ full_args.extend(args)
245
+
246
+ super().__init__(command=node_cmd, args=full_args, env=env, cwd=cwd)
247
+ self.script_path = script_path
248
+
249
+
250
+ class UvxStdioTransport(StdioTransport):
251
+ """Transport for running commands via the uvx tool."""
252
+
253
+ def __init__(
254
+ self,
255
+ tool_name: str,
256
+ tool_args: Optional[List[str]] = None,
257
+ project_directory: Optional[str] = None,
258
+ python_version: Optional[str] = None,
259
+ with_packages: Optional[List[str]] = None,
260
+ from_package: Optional[str] = None,
261
+ env_vars: Optional[Dict[str, str]] = None,
262
+ ):
263
+ """
264
+ Initialize a Uvx transport.
265
+
266
+ Args:
267
+ tool_name: Name of the tool to run via uvx
268
+ tool_args: Arguments to pass to the tool
269
+ project_directory: Project directory (for package resolution)
270
+ python_version: Python version to use
271
+ with_packages: Additional packages to include
272
+ from_package: Package to install the tool from
273
+ env_vars: Additional environment variables
274
+ """
275
+ # Basic validation
276
+ if project_directory and not Path(project_directory).exists():
277
+ raise NotADirectoryError(
278
+ f"Project directory not found: {project_directory}"
279
+ )
280
+
281
+ # Build uvx arguments
282
+ uvx_args = []
283
+ if python_version:
284
+ uvx_args.extend(["--python", python_version])
285
+ if from_package:
286
+ uvx_args.extend(["--from", from_package])
287
+ for pkg in with_packages or []:
288
+ uvx_args.extend(["--with", pkg])
289
+
290
+ # Add the tool name and tool args
291
+ uvx_args.append(tool_name)
292
+ if tool_args:
293
+ uvx_args.extend(tool_args)
294
+
295
+ # Get environment with any additional variables
296
+ env = None
297
+ if env_vars:
298
+ env = os.environ.copy()
299
+ env.update(env_vars)
300
+
301
+ super().__init__(command="uvx", args=uvx_args, env=env, cwd=project_directory)
302
+ self.tool_name = tool_name
303
+
304
+
305
+ class NpxStdioTransport(StdioTransport):
306
+ """Transport for running commands via the npx tool."""
307
+
308
+ def __init__(
309
+ self,
310
+ package: str,
311
+ args: Optional[List[str]] = None,
312
+ project_directory: Optional[str] = None,
313
+ env_vars: Optional[Dict[str, str]] = None,
314
+ use_package_lock: bool = True,
315
+ ):
316
+ """
317
+ Initialize an Npx transport.
318
+
319
+ Args:
320
+ package: Name of the npm package to run
321
+ args: Arguments to pass to the package command
322
+ project_directory: Project directory with package.json
323
+ env_vars: Additional environment variables
324
+ use_package_lock: Whether to use package-lock.json (--prefer-offline)
325
+ """
326
+ # Basic validation
327
+ if project_directory and not Path(project_directory).exists():
328
+ raise NotADirectoryError(
329
+ f"Project directory not found: {project_directory}"
330
+ )
331
+
332
+ # Build npx arguments
333
+ npx_args = []
334
+ if use_package_lock:
335
+ npx_args.append("--prefer-offline")
336
+
337
+ # Add the package name and args
338
+ npx_args.append(package)
339
+ if args:
340
+ npx_args.extend(args)
341
+
342
+ # Get environment with any additional variables
343
+ env = None
344
+ if env_vars:
345
+ env = os.environ.copy()
346
+ env.update(env_vars)
347
+
348
+ super().__init__(command="npx", args=npx_args, env=env, cwd=project_directory)
349
+ self.package = package
350
+
351
+
352
+ class FastMCPTransport(ClientTransport):
353
+ """
354
+ Special transport for in-memory connections to an MCP server.
355
+
356
+ This is particularly useful for testing or when client and server
357
+ are in the same process.
358
+ """
359
+
360
+ def __init__(self, mcp: FastMCPServer):
361
+ self._fastmcp = mcp # Can be FastMCP or MCPServer
362
+
363
+ @contextlib.asynccontextmanager
364
+ async def connect_session(
365
+ self, **session_kwargs: Unpack[SessionKwargs]
366
+ ) -> AsyncIterator[ClientSession]:
367
+ # create_connected_server_and_client_session manages the session lifecycle itself
368
+ async with create_connected_server_and_client_session(
369
+ server=self._fastmcp._mcp_server,
370
+ **session_kwargs,
371
+ ) as session:
372
+ yield session
373
+
374
+ def __repr__(self) -> str:
375
+ return f"<FastMCP(server='{self._fastmcp.name}')>"
376
+
377
+
378
+ def infer_transport(
379
+ transport: ClientTransport | FastMCPServer | AnyUrl | Path | str,
380
+ ) -> ClientTransport:
381
+ """
382
+ Infer the appropriate transport type from the given transport argument.
383
+
384
+ This function attempts to infer the correct transport type from the provided
385
+ argument, handling various input types and converting them to the appropriate
386
+ ClientTransport subclass.
387
+ """
388
+ # the transport is already a ClientTransport
389
+ if isinstance(transport, ClientTransport):
390
+ return transport
391
+
392
+ # the transport is a FastMCP server
393
+ elif isinstance(transport, FastMCPServer):
394
+ return FastMCPTransport(mcp=transport)
395
+
396
+ # the transport is a path to a script
397
+ elif isinstance(transport, (Path, str)) and Path(transport).exists():
398
+ if str(transport).endswith(".py"):
399
+ return PythonStdioTransport(script_path=transport)
400
+ elif str(transport).endswith(".js"):
401
+ return NodeStdioTransport(script_path=transport)
402
+ else:
403
+ raise ValueError(f"Unsupported script type: {transport}")
404
+
405
+ # the transport is an http(s) URL
406
+ elif isinstance(transport, (AnyUrl, str)) and str(transport).startswith("http"):
407
+ return SSETransport(url=transport)
408
+
409
+ # the transport is a websocket URL
410
+ elif isinstance(transport, (AnyUrl, str)) and str(transport).startswith("ws"):
411
+ return WSTransport(url=transport)
412
+
413
+ # the transport is an unknown type
414
+ else:
415
+ raise ValueError(f"Could not infer a valid transport from: {transport}")
src/fastmcp/clients/__init__.py DELETED
@@ -1,12 +0,0 @@
1
- from .websocket import WebSocketClient
2
- from .sse import SSEClient
3
- from .stdio import StdioClient, UvxClient
4
- from .fastmcp_client import FastMCPClient
5
-
6
- __all__ = [
7
- "StdioClient",
8
- "SSEClient",
9
- "WebSocketClient",
10
- "UvxClient",
11
- "FastMCPClient",
12
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
src/fastmcp/clients/fastmcp_client.py DELETED
@@ -1,50 +0,0 @@
1
- import contextlib
2
- from typing import TypeVar
3
-
4
- from mcp.shared.memory import create_connected_server_and_client_session
5
- from typing_extensions import Unpack
6
-
7
- from fastmcp.clients.base import BaseClient, ClientKwargs
8
- from fastmcp.server.server import FastMCP
9
-
10
- T = TypeVar("T")
11
-
12
-
13
- class FastMCPClient(BaseClient):
14
- """Client that connects directly to an in-memory FastMCP server.
15
-
16
- This client creates and manages an in-memory connection to a server,
17
- without using any external processes or network connections.
18
- """
19
-
20
- def __init__(
21
- self,
22
- server: FastMCP,
23
- **kwargs: Unpack[ClientKwargs],
24
- ):
25
- """Initialize an InMemoryClient that connects to an in-memory MCP server.
26
-
27
- Args:
28
- server: The FastMCP instance to connect to
29
- **kwargs: Additional arguments for BaseClient
30
- """
31
- super().__init__(**kwargs)
32
- self.server = server
33
- self._cm_session = None
34
-
35
- @contextlib.asynccontextmanager
36
- async def _connect(self):
37
- """Set up in-memory connection and session"""
38
- self._cm_session = create_connected_server_and_client_session(
39
- server=self.server._mcp_server,
40
- read_timeout_seconds=self._read_timeout_seconds,
41
- sampling_callback=self._sampling_callback,
42
- list_roots_callback=self._list_roots_callback,
43
- logging_callback=self._logging_callback,
44
- message_handler=self._message_handler,
45
- )
46
-
47
- async with self._cm_session as session:
48
- # No need to call initialize as create_connected_server_and_client_session already does
49
- async with self._set_session((None, None), session):
50
- yield self
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/fastmcp/clients/sse.py DELETED
@@ -1,32 +0,0 @@
1
- import contextlib
2
-
3
- from mcp import ClientSession
4
- from mcp.client.sse import sse_client
5
- from typing_extensions import Unpack
6
-
7
- from fastmcp.clients.base import BaseClient, ClientKwargs
8
-
9
-
10
- class SSEClient(BaseClient):
11
- def __init__(
12
- self,
13
- url: str,
14
- headers: dict[str, str] | None = None,
15
- **kwargs: Unpack[ClientKwargs],
16
- ):
17
- super().__init__(**kwargs)
18
- self.url = url
19
- self.headers = headers or {}
20
-
21
- @contextlib.asynccontextmanager
22
- async def _connect(self):
23
- """Set up SSE connection and session"""
24
- async with sse_client(self.url, headers=self.headers) as transport:
25
- read_stream, write_stream = transport
26
- async with ClientSession(
27
- read_stream=read_stream,
28
- write_stream=write_stream,
29
- **self._session_kwargs(),
30
- ) as session:
31
- async with self._set_session(transport, session):
32
- yield self
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/fastmcp/clients/stdio.py DELETED
@@ -1,134 +0,0 @@
1
- import contextlib
2
- import os
3
-
4
- from mcp import ClientSession, StdioServerParameters
5
- from mcp.client.stdio import stdio_client
6
- from typing_extensions import Unpack
7
-
8
- from fastmcp.clients.base import BaseClient, ClientKwargs
9
-
10
-
11
- class StdioClient(BaseClient):
12
- def __init__(
13
- self,
14
- server_script_path: str,
15
- **kwargs: Unpack[ClientKwargs],
16
- ):
17
- super().__init__(**kwargs)
18
- self.server_script_path = server_script_path
19
-
20
- @contextlib.asynccontextmanager
21
- async def _connect(self):
22
- """Set up stdio connection and session"""
23
- is_python = self.server_script_path.endswith(".py")
24
- is_js = self.server_script_path.endswith(".js")
25
- if not (is_python or is_js):
26
- raise ValueError("Server script must be a .py or .js file")
27
-
28
- command = "python" if is_python else "node"
29
- server_params = StdioServerParameters(
30
- command=command, args=[self.server_script_path], env=None
31
- )
32
-
33
- async with stdio_client(server_params) as transport:
34
- stdio, write = transport
35
-
36
- async with ClientSession(
37
- read_stream=stdio,
38
- write_stream=write,
39
- **self._session_kwargs(),
40
- ) as session:
41
- async with self._set_session(transport, session):
42
- yield self
43
-
44
-
45
- class UvxClient(BaseClient):
46
- """Client that uses uvx to run Python tools in isolated environments.
47
-
48
- uvx automatically installs and manages dependencies from pyproject.toml.
49
- """
50
-
51
- def __init__(
52
- self,
53
- tool_name: str,
54
- tool_args: list[str] | None = None,
55
- project_directory: str | None = None,
56
- python_version: str | None = None,
57
- with_packages: list[str] | None = None,
58
- from_package: str | None = None,
59
- env_vars: dict[str, str] | None = None,
60
- **kwargs: Unpack[ClientKwargs],
61
- ):
62
- """Initialize a UvxClient that uses uvx to run Python tools in isolated environments.
63
-
64
- Args:
65
- tool_name: Name of the tool/command to run
66
- tool_args: Arguments to pass to the tool
67
- project_directory: Path to the project directory (optional)
68
- python_version: Specific Python version to use (e.g., "3.10")
69
- with_packages: Additional packages to include
70
- from_package: Package that provides the tool if different from tool_name
71
- env_vars: Environment variables to set for the process
72
- **kwargs: Additional arguments for BaseClient
73
- """
74
- super().__init__(**kwargs)
75
- self.tool_name = tool_name
76
- self.tool_args = tool_args or []
77
- self.project_directory = project_directory
78
- self.python_version = python_version
79
- self.with_packages = with_packages or []
80
- self.from_package = from_package
81
- self.env_vars = env_vars or {}
82
-
83
- @contextlib.asynccontextmanager
84
- async def _connect(self):
85
- """Set up uvx connection and session"""
86
- # Check if project directory exists if provided
87
- if self.project_directory and not os.path.isdir(self.project_directory):
88
- raise ValueError(
89
- f"Project directory does not exist: {self.project_directory}"
90
- )
91
-
92
- # Build the uvx command arguments
93
- args = []
94
-
95
- # Add Python version if specified
96
- if self.python_version:
97
- args.extend(["--python", self.python_version])
98
-
99
- # Add from package if specified
100
- if self.from_package:
101
- args.extend(["--from", self.from_package])
102
-
103
- # Add with packages if specified
104
- for pkg in self.with_packages:
105
- args.extend(["--with", pkg])
106
-
107
- # Add the tool name
108
- args.append(self.tool_name)
109
-
110
- # Add the tool arguments
111
- args.extend(self.tool_args)
112
-
113
- # Create environment variables dictionary
114
- env = os.environ.copy()
115
- env.update(self.env_vars)
116
-
117
- # Configure the server parameters
118
- server_params = StdioServerParameters(
119
- command="uvx",
120
- args=args,
121
- env=env,
122
- cwd=self.project_directory,
123
- )
124
-
125
- async with stdio_client(server_params) as transport:
126
- stdio, write = transport
127
-
128
- async with ClientSession(
129
- read_stream=stdio,
130
- write_stream=write,
131
- **self._session_kwargs(),
132
- ) as session:
133
- async with self._set_session(transport, session):
134
- yield self
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/fastmcp/clients/websocket.py DELETED
@@ -1,31 +0,0 @@
1
- import contextlib
2
-
3
- from mcp import ClientSession
4
- from mcp.client.websocket import websocket_client
5
- from typing_extensions import Unpack
6
-
7
- from fastmcp.clients.base import BaseClient, ClientKwargs
8
-
9
-
10
- class WebSocketClient(BaseClient):
11
- def __init__(
12
- self,
13
- url: str,
14
- **kwargs: Unpack[ClientKwargs],
15
- ):
16
- super().__init__(**kwargs)
17
- self.url = url
18
-
19
- @contextlib.asynccontextmanager
20
- async def _connect(self):
21
- """Set up WebSocket connection and session"""
22
- async with websocket_client(self.url) as transport:
23
- read_stream, write_stream = transport
24
-
25
- async with ClientSession(
26
- read_stream=read_stream,
27
- write_stream=write_stream,
28
- **self._session_kwargs(),
29
- ) as session:
30
- async with self._set_session(transport, session):
31
- yield self
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/fastmcp/server/proxy.py CHANGED
@@ -3,7 +3,8 @@ from typing import Any, cast
3
  import mcp.types
4
  from mcp.types import BlobResourceContents, PromptMessage, TextResourceContents
5
 
6
- from fastmcp.clients.base import BaseClient
 
7
  from fastmcp.prompts import Prompt
8
  from fastmcp.resources import Resource, ResourceTemplate
9
  from fastmcp.server.context import Context
@@ -20,14 +21,12 @@ def _proxy_passthrough():
20
 
21
 
22
  class ProxyTool(Tool):
23
- def __init__(self, client: "BaseClient", **kwargs):
24
  super().__init__(**kwargs)
25
  self._client = client
26
 
27
  @classmethod
28
- async def from_client(
29
- cls, client: "BaseClient", tool: mcp.types.Tool
30
- ) -> "ProxyTool":
31
  return cls(
32
  client=client,
33
  name=tool.name,
@@ -50,7 +49,7 @@ class ProxyTool(Tool):
50
 
51
  class ProxyResource(Resource):
52
  def __init__(
53
- self, client: "BaseClient", *, _value: str | bytes | None = None, **kwargs
54
  ):
55
  super().__init__(**kwargs)
56
  self._client = client
@@ -58,7 +57,7 @@ class ProxyResource(Resource):
58
 
59
  @classmethod
60
  async def from_client(
61
- cls, client: "BaseClient", resource: mcp.types.Resource
62
  ) -> "ProxyResource":
63
  return cls(
64
  client=client,
@@ -83,13 +82,13 @@ class ProxyResource(Resource):
83
 
84
 
85
  class ProxyTemplate(ResourceTemplate):
86
- def __init__(self, client: "BaseClient", **kwargs):
87
  super().__init__(**kwargs)
88
  self._client = client
89
 
90
  @classmethod
91
  async def from_client(
92
- cls, client: "BaseClient", template: mcp.types.ResourceTemplate
93
  ) -> "ProxyTemplate":
94
  return cls(
95
  client=client,
@@ -123,13 +122,13 @@ class ProxyTemplate(ResourceTemplate):
123
 
124
 
125
  class ProxyPrompt(Prompt):
126
- def __init__(self, client: "BaseClient", **kwargs):
127
  super().__init__(**kwargs)
128
  self._client = client
129
 
130
  @classmethod
131
  async def from_client(
132
- cls, client: "BaseClient", prompt: mcp.types.Prompt
133
  ) -> "ProxyPrompt":
134
  return cls(
135
  client=client,
@@ -155,7 +154,10 @@ class FastMCPProxy(FastMCP):
155
 
156
  @classmethod
157
  async def from_client(
158
- cls, client: "BaseClient", name: str | None = None, **settings: Any
 
 
 
159
  ) -> "FastMCPProxy":
160
  """Create a FastMCP proxy server from a client.
161
 
@@ -210,3 +212,8 @@ class FastMCPProxy(FastMCP):
210
 
211
  logger.info(f"Created server '{server.name}' proxying to client: {client}")
212
  return server
 
 
 
 
 
 
3
  import mcp.types
4
  from mcp.types import BlobResourceContents, PromptMessage, TextResourceContents
5
 
6
+ import fastmcp
7
+ from fastmcp.client import Client
8
  from fastmcp.prompts import Prompt
9
  from fastmcp.resources import Resource, ResourceTemplate
10
  from fastmcp.server.context import Context
 
21
 
22
 
23
  class ProxyTool(Tool):
24
+ def __init__(self, client: "Client", **kwargs):
25
  super().__init__(**kwargs)
26
  self._client = client
27
 
28
  @classmethod
29
+ async def from_client(cls, client: "Client", tool: mcp.types.Tool) -> "ProxyTool":
 
 
30
  return cls(
31
  client=client,
32
  name=tool.name,
 
49
 
50
  class ProxyResource(Resource):
51
  def __init__(
52
+ self, client: "Client", *, _value: str | bytes | None = None, **kwargs
53
  ):
54
  super().__init__(**kwargs)
55
  self._client = client
 
57
 
58
  @classmethod
59
  async def from_client(
60
+ cls, client: "Client", resource: mcp.types.Resource
61
  ) -> "ProxyResource":
62
  return cls(
63
  client=client,
 
82
 
83
 
84
  class ProxyTemplate(ResourceTemplate):
85
+ def __init__(self, client: "Client", **kwargs):
86
  super().__init__(**kwargs)
87
  self._client = client
88
 
89
  @classmethod
90
  async def from_client(
91
+ cls, client: "Client", template: mcp.types.ResourceTemplate
92
  ) -> "ProxyTemplate":
93
  return cls(
94
  client=client,
 
122
 
123
 
124
  class ProxyPrompt(Prompt):
125
+ def __init__(self, client: "Client", **kwargs):
126
  super().__init__(**kwargs)
127
  self._client = client
128
 
129
  @classmethod
130
  async def from_client(
131
+ cls, client: "Client", prompt: mcp.types.Prompt
132
  ) -> "ProxyPrompt":
133
  return cls(
134
  client=client,
 
154
 
155
  @classmethod
156
  async def from_client(
157
+ cls,
158
+ client: "Client",
159
+ name: str | None = None,
160
+ **settings: fastmcp.settings.ServerSettings,
161
  ) -> "FastMCPProxy":
162
  """Create a FastMCP proxy server from a client.
163
 
 
212
 
213
  logger.info(f"Created server '{server.name}' proxying to client: {client}")
214
  return server
215
+
216
+ @classmethod
217
+ async def from_server(cls, server: FastMCP, **settings: Any) -> "FastMCPProxy":
218
+ client = Client(transport=fastmcp.client.transports.FastMCPTransport(server))
219
+ return await cls.from_client(client, **settings)
src/fastmcp/server/server.py CHANGED
@@ -50,11 +50,10 @@ from fastmcp.utilities.logging import configure_logging, get_logger
50
  from fastmcp.utilities.types import Image
51
 
52
  if TYPE_CHECKING:
53
- from fastmcp.clients.base import BaseClient
54
  from fastmcp.server.context import Context
55
  from fastmcp.server.openapi import FastMCPOpenAPI
56
  from fastmcp.server.proxy import FastMCPProxy
57
-
58
  logger = get_logger(__name__)
59
 
60
 
@@ -117,20 +116,29 @@ class FastMCP(Generic[LifespanResultT]):
117
  def instructions(self) -> str | None:
118
  return self._mcp_server.instructions
119
 
120
- def run(self, transport: Literal["stdio", "sse"] = "stdio") -> None:
121
- """Run the FastMCP server. Note this is a synchronous function.
122
 
123
  Args:
124
  transport: Transport protocol to use ("stdio" or "sse")
125
  """
126
- TRANSPORTS = Literal["stdio", "sse"]
127
- if transport not in TRANSPORTS.__args__: # type: ignore
 
128
  raise ValueError(f"Unknown transport: {transport}")
129
 
130
  if transport == "stdio":
131
- anyio.run(self.run_stdio_async)
132
  else: # transport == "sse"
133
- anyio.run(self.run_sse_async)
 
 
 
 
 
 
 
 
134
 
135
  def _setup_handlers(self) -> None:
136
  """Set up core MCP protocol handlers."""
@@ -547,7 +555,9 @@ class FastMCP(Generic[LifespanResultT]):
547
  logger.debug(f"Imported prompts with prefix '{prompt_prefix}'")
548
 
549
  @classmethod
550
- async def as_proxy(cls, client: "BaseClient", **settings: Any) -> "FastMCPProxy":
 
 
551
  """
552
  Create a FastMCP proxy server from a client.
553
 
@@ -562,9 +572,18 @@ class FastMCP(Generic[LifespanResultT]):
562
  Returns:
563
  A FastMCP server that proxies requests to the client
564
  """
 
 
565
  from .proxy import FastMCPProxy
566
 
567
- return await FastMCPProxy.from_client(client=client, **settings)
 
 
 
 
 
 
 
568
 
569
  @classmethod
570
  def from_openapi(
 
50
  from fastmcp.utilities.types import Image
51
 
52
  if TYPE_CHECKING:
53
+ from fastmcp.client import Client
54
  from fastmcp.server.context import Context
55
  from fastmcp.server.openapi import FastMCPOpenAPI
56
  from fastmcp.server.proxy import FastMCPProxy
 
57
  logger = get_logger(__name__)
58
 
59
 
 
116
  def instructions(self) -> str | None:
117
  return self._mcp_server.instructions
118
 
119
+ async def run_async(self, transport: Literal["stdio", "sse"] | None = None) -> None:
120
+ """Run the FastMCP server asynchronously.
121
 
122
  Args:
123
  transport: Transport protocol to use ("stdio" or "sse")
124
  """
125
+ if transport is None:
126
+ transport = "stdio"
127
+ if transport not in ["stdio", "sse"]:
128
  raise ValueError(f"Unknown transport: {transport}")
129
 
130
  if transport == "stdio":
131
+ await self.run_stdio_async()
132
  else: # transport == "sse"
133
+ await self.run_sse_async()
134
+
135
+ def run(self, transport: Literal["stdio", "sse"] | None = None) -> None:
136
+ """Run the FastMCP server. Note this is a synchronous function.
137
+
138
+ Args:
139
+ transport: Transport protocol to use ("stdio" or "sse")
140
+ """
141
+ anyio.run(self.run_async, transport)
142
 
143
  def _setup_handlers(self) -> None:
144
  """Set up core MCP protocol handlers."""
 
555
  logger.debug(f"Imported prompts with prefix '{prompt_prefix}'")
556
 
557
  @classmethod
558
+ async def as_proxy(
559
+ cls, client: "Client | FastMCP", **settings: Any
560
+ ) -> "FastMCPProxy":
561
  """
562
  Create a FastMCP proxy server from a client.
563
 
 
572
  Returns:
573
  A FastMCP server that proxies requests to the client
574
  """
575
+ from fastmcp.client import Client
576
+
577
  from .proxy import FastMCPProxy
578
 
579
+ if isinstance(client, Client):
580
+ return await FastMCPProxy.from_client(client=client, **settings)
581
+
582
+ elif isinstance(client, FastMCP):
583
+ return await FastMCPProxy.from_server(server=client, **settings)
584
+
585
+ else:
586
+ raise ValueError(f"Unknown client type: {type(client)}")
587
 
588
  @classmethod
589
  def from_openapi(
tests/{clients → client}/__init__.py RENAMED
File without changes
tests/{clients/test_fastmcp_client.py → client/test_fastmcp_transport.py} RENAMED
@@ -3,7 +3,8 @@ from typing import cast
3
  import pytest
4
  from pydantic import AnyUrl
5
 
6
- from fastmcp.clients import FastMCPClient
 
7
  from fastmcp.server.server import FastMCP
8
 
9
 
@@ -44,7 +45,7 @@ def fastmcp_server():
44
 
45
  async def test_list_tools(fastmcp_server):
46
  """Test listing tools with InMemoryClient."""
47
- client = FastMCPClient(server=fastmcp_server)
48
 
49
  async with client:
50
  result = await client.list_tools()
@@ -56,7 +57,7 @@ async def test_list_tools(fastmcp_server):
56
 
57
  async def test_call_tool(fastmcp_server):
58
  """Test calling a tool with InMemoryClient."""
59
- client = FastMCPClient(server=fastmcp_server)
60
 
61
  async with client:
62
  result = await client.call_tool("greet", {"name": "World"})
@@ -68,7 +69,7 @@ async def test_call_tool(fastmcp_server):
68
 
69
  async def test_list_resources(fastmcp_server):
70
  """Test listing resources with InMemoryClient."""
71
- client = FastMCPClient(server=fastmcp_server)
72
 
73
  async with client:
74
  result = await client.list_resources()
@@ -80,7 +81,7 @@ async def test_list_resources(fastmcp_server):
80
 
81
  async def test_list_prompts(fastmcp_server):
82
  """Test listing prompts with InMemoryClient."""
83
- client = FastMCPClient(server=fastmcp_server)
84
 
85
  async with client:
86
  result = await client.list_prompts()
@@ -92,7 +93,7 @@ async def test_list_prompts(fastmcp_server):
92
 
93
  async def test_get_prompt(fastmcp_server):
94
  """Test getting a prompt with InMemoryClient."""
95
- client = FastMCPClient(server=fastmcp_server)
96
 
97
  async with client:
98
  result = await client.get_prompt("welcome", {"name": "Developer"})
@@ -104,7 +105,7 @@ async def test_get_prompt(fastmcp_server):
104
 
105
  async def test_read_resource(fastmcp_server):
106
  """Test reading a resource with InMemoryClient."""
107
- client = FastMCPClient(server=fastmcp_server)
108
 
109
  async with client:
110
  # Use the URI from the resource we know exists in our server
@@ -122,7 +123,7 @@ async def test_read_resource(fastmcp_server):
122
 
123
  async def test_client_connection(fastmcp_server):
124
  """Test that the client connects and disconnects properly."""
125
- client = FastMCPClient(server=fastmcp_server)
126
 
127
  # Before connection
128
  assert not client.is_connected()
@@ -137,7 +138,7 @@ async def test_client_connection(fastmcp_server):
137
 
138
  async def test_resource_template(fastmcp_server):
139
  """Test using a resource template with InMemoryClient."""
140
- client = FastMCPClient(server=fastmcp_server)
141
 
142
  async with client:
143
  # First, list templates
 
3
  import pytest
4
  from pydantic import AnyUrl
5
 
6
+ from fastmcp.client import Client
7
+ from fastmcp.client.transports import FastMCPTransport
8
  from fastmcp.server.server import FastMCP
9
 
10
 
 
45
 
46
  async def test_list_tools(fastmcp_server):
47
  """Test listing tools with InMemoryClient."""
48
+ client = Client(transport=FastMCPTransport(fastmcp_server))
49
 
50
  async with client:
51
  result = await client.list_tools()
 
57
 
58
  async def test_call_tool(fastmcp_server):
59
  """Test calling a tool with InMemoryClient."""
60
+ client = Client(transport=FastMCPTransport(fastmcp_server))
61
 
62
  async with client:
63
  result = await client.call_tool("greet", {"name": "World"})
 
69
 
70
  async def test_list_resources(fastmcp_server):
71
  """Test listing resources with InMemoryClient."""
72
+ client = Client(transport=FastMCPTransport(fastmcp_server))
73
 
74
  async with client:
75
  result = await client.list_resources()
 
81
 
82
  async def test_list_prompts(fastmcp_server):
83
  """Test listing prompts with InMemoryClient."""
84
+ client = Client(transport=FastMCPTransport(fastmcp_server))
85
 
86
  async with client:
87
  result = await client.list_prompts()
 
93
 
94
  async def test_get_prompt(fastmcp_server):
95
  """Test getting a prompt with InMemoryClient."""
96
+ client = Client(transport=FastMCPTransport(fastmcp_server))
97
 
98
  async with client:
99
  result = await client.get_prompt("welcome", {"name": "Developer"})
 
105
 
106
  async def test_read_resource(fastmcp_server):
107
  """Test reading a resource with InMemoryClient."""
108
+ client = Client(transport=FastMCPTransport(fastmcp_server))
109
 
110
  async with client:
111
  # Use the URI from the resource we know exists in our server
 
123
 
124
  async def test_client_connection(fastmcp_server):
125
  """Test that the client connects and disconnects properly."""
126
+ client = Client(transport=FastMCPTransport(fastmcp_server))
127
 
128
  # Before connection
129
  assert not client.is_connected()
 
138
 
139
  async def test_resource_template(fastmcp_server):
140
  """Test using a resource template with InMemoryClient."""
141
+ client = Client(transport=FastMCPTransport(fastmcp_server))
142
 
143
  async with client:
144
  # First, list templates
tests/server/test_proxy.py CHANGED
@@ -5,7 +5,8 @@ import pytest
5
  from dirty_equals import Contains
6
 
7
  from fastmcp import FastMCP
8
- from fastmcp.clients.fastmcp_client import FastMCPClient
 
9
  from fastmcp.server.proxy import FastMCPProxy
10
 
11
  USERS = [
@@ -62,13 +63,13 @@ def fastmcp_server():
62
  @pytest.fixture
63
  async def proxy_server(fastmcp_server):
64
  """Fixture that creates a FastMCP proxy server."""
65
- return await FastMCP.as_proxy(FastMCPClient(fastmcp_server))
66
 
67
 
68
  async def test_create_proxy(fastmcp_server):
69
  """Test that the proxy server properly forwards requests to the original server."""
70
  # Create a client
71
- client = FastMCPClient(fastmcp_server)
72
 
73
  server = await FastMCPProxy.from_client(client)
74
 
 
5
  from dirty_equals import Contains
6
 
7
  from fastmcp import FastMCP
8
+ from fastmcp.client import Client
9
+ from fastmcp.client.transports import FastMCPTransport
10
  from fastmcp.server.proxy import FastMCPProxy
11
 
12
  USERS = [
 
63
  @pytest.fixture
64
  async def proxy_server(fastmcp_server):
65
  """Fixture that creates a FastMCP proxy server."""
66
+ return await FastMCP.as_proxy(Client(transport=FastMCPTransport(fastmcp_server)))
67
 
68
 
69
  async def test_create_proxy(fastmcp_server):
70
  """Test that the proxy server properly forwards requests to the original server."""
71
  # Create a client
72
+ client = Client(transport=FastMCPTransport(fastmcp_server))
73
 
74
  server = await FastMCPProxy.from_client(client)
75
 
tests/server/test_run_server.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # from pathlib import Path
2
+ # from typing import TYPE_CHECKING, Any
3
+
4
+ # import pytest
5
+
6
+ # import fastmcp
7
+ # from fastmcp import FastMCP
8
+
9
+ # if TYPE_CHECKING:
10
+ # pass
11
+
12
+ # USERS = [
13
+ # {"id": "1", "name": "Alice", "active": True},
14
+ # {"id": "2", "name": "Bob", "active": True},
15
+ # {"id": "3", "name": "Charlie", "active": False},
16
+ # ]
17
+
18
+
19
+ # @pytest.fixture
20
+ # def fastmcp_server():
21
+ # server = FastMCP("TestServer")
22
+
23
+ # # --- Tools ---
24
+
25
+ # @server.tool()
26
+ # def greet(name: str) -> str:
27
+ # """Greet someone by name."""
28
+ # return f"Hello, {name}!"
29
+
30
+ # @server.tool()
31
+ # def add(a: int, b: int) -> int:
32
+ # """Add two numbers together."""
33
+ # return a + b
34
+
35
+ # @server.tool()
36
+ # def error_tool():
37
+ # """This tool always raises an error."""
38
+ # raise ValueError("This is a test error")
39
+
40
+ # # --- Resources ---
41
+
42
+ # @server.resource(uri="resource://wave")
43
+ # def wave() -> str:
44
+ # return "👋"
45
+
46
+ # @server.resource(uri="data://users")
47
+ # async def get_users() -> list[dict[str, Any]]:
48
+ # return USERS
49
+
50
+ # @server.resource(uri="data://user/{user_id}")
51
+ # async def get_user(user_id: str) -> dict[str, Any] | None:
52
+ # return next((user for user in USERS if user["id"] == user_id), None)
53
+
54
+ # # --- Prompts ---
55
+
56
+ # @server.prompt()
57
+ # def welcome(name: str) -> str:
58
+ # return f"Welcome to FastMCP, {name}!"
59
+
60
+ # return server
61
+
62
+
63
+ # @pytest.fixture
64
+ # async def stdio_client():
65
+ # # Find the stdio.py script path
66
+ # base_dir = Path(__file__).parent
67
+ # stdio_script = base_dir / "test_servers" / "stdio.py"
68
+
69
+ # if not stdio_script.exists():
70
+ # raise FileNotFoundError(f"Could not find stdio.py script at {stdio_script}")
71
+
72
+ # client = fastmcp.Client(
73
+ # transport=fastmcp.client.transports.StdioTransport(
74
+ # command="python",
75
+ # args=[str(stdio_script)],
76
+ # )
77
+ # )
78
+
79
+ # async with client:
80
+ # print("READY")
81
+ # yield client
82
+ # print("DONE")
83
+
84
+
85
+ # class TestRunServerStdio:
86
+ # async def test_run_server_stdio(
87
+ # self, fastmcp_server: FastMCP, stdio_client: fastmcp.Client
88
+ # ):
89
+ # print("TEST")
90
+ # tools = await stdio_client.list_tools()
91
+ # print("TEST 2")
92
+ # assert tools == 1
93
+
94
+
95
+ # class TestRunServerSSE:
96
+ # @pytest.mark.anyio
97
+ # async def test_run_server_sse(self, fastmcp_server: FastMCP):
98
+ # pass
tests/server/test_servers/fastmcp_server.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any
2
+
3
+ from fastmcp import FastMCP
4
+
5
+ USERS = [
6
+ {"id": "1", "name": "Alice", "active": True},
7
+ {"id": "2", "name": "Bob", "active": True},
8
+ {"id": "3", "name": "Charlie", "active": False},
9
+ ]
10
+
11
+
12
+ server = FastMCP("TestServer")
13
+
14
+ # --- Tools ---
15
+
16
+
17
+ @server.tool()
18
+ def greet(name: str) -> str:
19
+ """Greet someone by name."""
20
+ return f"Hello, {name}!"
21
+
22
+
23
+ @server.tool()
24
+ def add(a: int, b: int) -> int:
25
+ """Add two numbers together."""
26
+ return a + b
27
+
28
+
29
+ @server.tool()
30
+ def error_tool():
31
+ """This tool always raises an error."""
32
+ raise ValueError("This is a test error")
33
+
34
+
35
+ # --- Resources ---
36
+
37
+
38
+ @server.resource(uri="resource://wave")
39
+ def wave() -> str:
40
+ return "👋"
41
+
42
+
43
+ @server.resource(uri="data://users")
44
+ async def get_users() -> list[dict[str, Any]]:
45
+ return USERS
46
+
47
+
48
+ @server.resource(uri="data://user/{user_id}")
49
+ async def get_user(user_id: str) -> dict[str, Any] | None:
50
+ return next((user for user in USERS if user["id"] == user_id), None)
51
+
52
+
53
+ # --- Prompts ---
54
+
55
+
56
+ @server.prompt()
57
+ def welcome(name: str) -> str:
58
+ return f"Welcome to FastMCP, {name}!"
tests/server/test_servers/sse.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import asyncio
2
+
3
+ import fastmcp_server
4
+
5
+ if __name__ == "__main__":
6
+ asyncio.run(fastmcp_server.server.run_sse_async())
tests/server/test_servers/stdio.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import asyncio
2
+
3
+ import fastmcp_server
4
+
5
+ if __name__ == "__main__":
6
+ asyncio.run(fastmcp_server.server.run_stdio_async())