Skip to content

Commit a93226a

Browse files
Merge pull request #5 from promptmesh/feat/in-process-fastmcp
support in process fastmcp servers
2 parents 57c83ca + b019d66 commit a93226a

File tree

9 files changed

+364
-106
lines changed

9 files changed

+364
-106
lines changed

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ build-backend = "hatchling.build"
2424

2525
[dependency-groups]
2626
dev = [
27+
"fastmcp-test>=0.1.0",
2728
"mypy>=1.15.0",
2829
"pytest>=8.3.5",
2930
"pytest-asyncio>=0.26.0",

src/easymcp/client/SessionMaker.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from typing import TypeAlias
22

33
from easymcp.client.sessions.GenericSession import BaseSessionProtocol
4+
from easymcp.client.sessions.fastmcp.main import FastMCPSession
5+
from easymcp.client.sessions.fastmcp.parameters import FastMcpParameters
46
from easymcp.client.sessions.mcp import MCPClientSession
57
from easymcp.client.transports.stdio import StdioTransport, StdioServerParameters
68
from easymcp.client.transports.docker import DockerTransport, DockerServerParameters
@@ -9,7 +11,9 @@
911

1012
transportTypes: TypeAlias = StdioServerParameters | DockerServerParameters | SseServerParameters
1113

12-
def make_transport(arguments: transportTypes) -> BaseSessionProtocol:
14+
make_transport_input: TypeAlias = transportTypes | FastMcpParameters
15+
16+
def make_transport(arguments: make_transport_input) -> BaseSessionProtocol:
1317

1418
if isinstance(arguments, StdioServerParameters):
1519
return MCPClientSession(StdioTransport(arguments))
@@ -20,4 +24,7 @@ def make_transport(arguments: transportTypes) -> BaseSessionProtocol:
2024
if isinstance(arguments, SseServerParameters):
2125
return MCPClientSession(SseTransport(arguments))
2226

27+
if isinstance(arguments, FastMcpParameters):
28+
return FastMCPSession(arguments)
29+
2330
raise ValueError(f"Unknown transport type: {type(arguments)}")

src/easymcp/client/sessions/fastmcp/__init__.py

Whitespace-only changes.
+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import importlib
2+
import os
3+
import sys
4+
from mcp import ListPromptsResult, ListResourcesResult, ListToolsResult, types
5+
from mcp.server.fastmcp import FastMCP
6+
7+
from easymcp.client.sessions.GenericSession import (
8+
BaseSessionProtocol,
9+
PromptsCompatible,
10+
ResourcesCompatible,
11+
ToolsCompatible,
12+
)
13+
from easymcp.client.sessions.fastmcp.parameters import FastMcpParameters
14+
15+
16+
class FastMCPSession(
17+
BaseSessionProtocol, ToolsCompatible, ResourcesCompatible, PromptsCompatible
18+
):
19+
"""ASGI style fastmcp session"""
20+
21+
params: FastMcpParameters
22+
session: FastMCP
23+
24+
def __init__(self, params: FastMcpParameters):
25+
self.params = params
26+
27+
async def init(self) -> None:
28+
"""Initialize the session"""
29+
moduleName, identifier = self.params.module.rsplit(":", 1)
30+
31+
originalEnv = os.environ
32+
originalArgv = sys.argv.copy()
33+
34+
mcpEnv = os.environ.copy()
35+
mcpEnv.update(self.params.env)
36+
mcpArgv = [
37+
"uvx",
38+
]
39+
mcpArgv.extend(self.params.argv)
40+
41+
os.environ = mcpEnv # type: ignore
42+
sys.argv = mcpArgv
43+
44+
try:
45+
module = importlib.import_module(moduleName)
46+
47+
cls = getattr(module, identifier)
48+
if self.params.factory:
49+
self.session = cls()
50+
else:
51+
self.session = cls
52+
53+
except ModuleNotFoundError as e:
54+
raise ImportError(f"Module {moduleName} not found") from e
55+
56+
except AttributeError as e:
57+
raise ImportError(
58+
f"Module {moduleName} does not contain {identifier}"
59+
) from e
60+
61+
except ImportError as e:
62+
raise ImportError(f"Error importing {moduleName}") from e
63+
64+
finally:
65+
os.environ = originalEnv
66+
sys.argv = originalArgv
67+
68+
assert isinstance(self.session, FastMCP), "Session must be a FastMCP instance"
69+
70+
async def list_prompts(self, force: bool = False) -> ListPromptsResult:
71+
"""List all prompts"""
72+
return ListPromptsResult(prompts=await self.session.list_prompts())
73+
74+
async def list_resources(self, force: bool = False) -> ListResourcesResult:
75+
"""List all responses"""
76+
return ListResourcesResult(resources=await self.session.list_resources())
77+
78+
async def list_tools(self, force: bool = False) -> ListToolsResult:
79+
"""List all tools"""
80+
return ListToolsResult(tools=await self.session.list_tools())
81+
82+
async def read_prompt(self, prompt_name: str, args: dict) -> types.GetPromptResult:
83+
"""Read a prompt"""
84+
return await self.session.get_prompt(prompt_name, args)
85+
86+
async def read_resource(self, resource_name: str) -> types.ReadResourceResult:
87+
"""Read a resource"""
88+
content = await self.session.read_resource(resource_name)
89+
data = [
90+
{
91+
"mimeType": c.mime_type,
92+
"text": c.content,
93+
"uri": resource_name,
94+
}
95+
for c in content
96+
]
97+
result = {
98+
"contents": data,
99+
}
100+
return types.ReadResourceResult.model_validate(result)
101+
102+
async def call_tool(self, tool_name: str, args: dict) -> types.CallToolResult:
103+
"""Call a tool"""
104+
content = await self.session.call_tool(tool_name, args)
105+
return types.CallToolResult(content=list(content))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from pydantic import BaseModel
2+
3+
class FastMcpParameters(BaseModel):
4+
module: str
5+
factory: bool = False
6+
env: dict = {}
7+
argv: list = []

tests/fastmcp_session.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from easymcp.client.sessions.fastmcp.main import FastMCPSession
2+
from easymcp.client.sessions.fastmcp.parameters import FastMcpParameters
3+
4+
5+
6+
config = FastMcpParameters(module="fastmcp_test:mcp")
7+
8+
async def main():
9+
session = FastMCPSession(config)
10+
await session.init()
11+
print(await session.list_tools())
12+
print()
13+
print(await session.call_tool("get_random_bool", {}))
14+
print()
15+
print(await session.list_resources())
16+
print()
17+
# print(await session.read_resource("demo://lorem-ipsum"))
18+
print()
19+
print(await session.list_prompts())
20+
21+
if __name__ == "__main__":
22+
import asyncio
23+
asyncio.run(main())

tests/test_client_manager.py

+28-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22
from mcp.types import CallToolResult, ReadResourceResult
33
from easymcp.client.ClientManager import ClientManager
4+
from easymcp.client.sessions.fastmcp.parameters import FastMcpParameters
45
from easymcp.client.transports.stdio import StdioServerParameters
56

67
@pytest.mark.asyncio
@@ -9,6 +10,7 @@ async def test_client_manager_operations():
910

1011
searxng = StdioServerParameters(command="uvx", args=["mcp-searxng"])
1112
timeserver = StdioServerParameters(command="uvx", args=["mcp-timeserver"])
13+
fastmcpserver = FastMcpParameters(module="fastmcp_test:mcp")
1214

1315
servers = {
1416
"searxng": searxng,
@@ -21,18 +23,43 @@ async def test_client_manager_operations():
2123

2224
tools = await mgr.list_tools()
2325
assert isinstance(tools, list)
26+
assert len(tools) > 0
27+
assert 'searxng.search' in [tool.name for tool in tools]
28+
assert 'timeserver.get-current-time' in [tool.name for tool in tools]
2429

2530
result = await mgr.call_tool("timeserver.get-current-time", {})
2631
assert isinstance(result, CallToolResult)
2732

2833
resources = await mgr.list_resources()
2934
assert isinstance(resources, list)
35+
assert len(resources) > 0
36+
assert 'mcp-timeserver+datetime://Africa/Algiers/now' in [str(resource.uri) for resource in resources]
3037

3138
resource = await mgr.read_resource("mcp-timeserver+datetime://Africa/Algiers/now")
3239
assert isinstance(resource, ReadResourceResult)
3340

3441
await mgr.remove_server("searxng")
42+
assert "timeserver" in mgr.list_servers()
3543
assert "searxng" not in mgr.list_servers()
3644

45+
tools = await mgr.list_tools()
46+
assert isinstance(tools, list)
47+
assert len(tools) > 0
48+
assert 'searxng.search' not in [tool.name for tool in tools]
49+
assert 'timeserver.get-current-time' in [tool.name for tool in tools]
50+
3751
await mgr.add_server("searxng", searxng)
38-
assert "searxng" in mgr.list_servers()
52+
assert "searxng" in mgr.list_servers()
53+
assert "timeserver" in mgr.list_servers()
54+
assert "fastmcpserver" not in mgr.list_servers()
55+
56+
await mgr.add_server("fastmcpserver", fastmcpserver)
57+
assert "fastmcpserver" in mgr.list_servers()
58+
59+
resources = await mgr.list_resources()
60+
assert isinstance(resources, list)
61+
assert len(resources) > 0
62+
63+
tools = await mgr.list_tools()
64+
assert isinstance(tools, list)
65+
assert len(tools) > 0

tests/test_fastmcp_session.py

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from mcp import ListResourcesResult, ListToolsResult, ReadResourceResult
2+
from mcp.types import CallToolResult
3+
import pytest
4+
5+
from easymcp.client.sessions.fastmcp.main import FastMCPSession
6+
from easymcp.client.sessions.fastmcp.parameters import FastMcpParameters
7+
8+
# test valid module
9+
@pytest.mark.asyncio
10+
async def test_fastmcp_session():
11+
params = FastMcpParameters(module="fastmcp_test:mcp")
12+
session = FastMCPSession(params)
13+
await session.init()
14+
assert session.session is not None
15+
16+
# tools
17+
tools = await session.list_tools()
18+
assert isinstance(tools, ListToolsResult)
19+
assert len(tools.tools) > 0
20+
for tool in tools.tools:
21+
assert tool.name is not None
22+
assert tool.inputSchema is not None
23+
24+
tool_call = await session.call_tool(tool_name="get_random_bool", args={})
25+
assert isinstance(tool_call, CallToolResult)
26+
assert isinstance(tool_call.content, list)
27+
assert len(tool_call.content) > 0
28+
assert tool_call.isError is False
29+
30+
# resources
31+
resources = await session.list_resources()
32+
assert isinstance(resources, ListResourcesResult)
33+
assert len(resources.resources) > 0
34+
for resource in resources.resources:
35+
assert resource.name is not None
36+
37+
resolved_resource = await session.read_resource(resource_name="demo://random-number")
38+
assert isinstance(resolved_resource, ReadResourceResult)
39+
assert isinstance(resolved_resource.contents, list)
40+
assert len(resolved_resource.contents) > 0
41+
42+
43+
# test missing fastmcp class
44+
@pytest.mark.asyncio
45+
async def test_fastmcp_session_missing_fastmcp_class():
46+
params = FastMcpParameters(module="fastmcp_test:invalid")
47+
session = FastMCPSession(params)
48+
with pytest.raises(ImportError, match="Module fastmcp_test does not contain invalid"):
49+
await session.init()
50+
51+
# test invalid module
52+
@pytest.mark.asyncio
53+
async def test_fastmcp_session_invalid_module():
54+
params = FastMcpParameters(module="invalid:mcp")
55+
session = FastMCPSession(params)
56+
with pytest.raises(ImportError, match="Module invalid not found"):
57+
await session.init()

0 commit comments

Comments
 (0)