Skip to content

Commit d2a044d

Browse files
committed
2 parents e54a24a + acf9a92 commit d2a044d

File tree

3 files changed

+199
-2
lines changed

3 files changed

+199
-2
lines changed
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
"""
2+
Hosted MCP Server implementation for PraisonAI Agents.
3+
4+
This module provides a base class for creating hosted MCP servers
5+
that can handle requests and integrate with the MCP protocol.
6+
7+
Note: This is an example implementation. To use it, ensure you have installed:
8+
pip install praisonaiagents[mcp] starlette>=0.27.0
9+
"""
10+
11+
import asyncio
12+
import logging
13+
from typing import Dict, Any, Optional, List, Callable
14+
import json
15+
16+
try:
17+
from mcp.server.fastmcp import FastMCP
18+
from mcp.server import Server
19+
from starlette.applications import Starlette
20+
from starlette.requests import Request
21+
from starlette.routing import Mount, Route
22+
from mcp.server.sse import SseServerTransport
23+
import uvicorn
24+
except ImportError:
25+
raise ImportError(
26+
"MCP server dependencies not installed. "
27+
"Please install with: pip install praisonaiagents[mcp] starlette>=0.27.0"
28+
)
29+
30+
logger = logging.getLogger(__name__)
31+
32+
33+
class HostedMCPServer:
34+
"""
35+
Base class for creating hosted MCP servers.
36+
37+
This class provides a foundation for building MCP servers that can:
38+
- Handle incoming requests
39+
- Define custom tools
40+
- Support SSE transport
41+
- Be extended with custom functionality like latency tracking
42+
"""
43+
44+
def __init__(self, name: str = "hosted-mcp-server", host: str = "localhost", port: int = 8080):
45+
"""
46+
Initialize the hosted MCP server.
47+
48+
Args:
49+
name: Server name for identification
50+
host: Host to bind to (default: localhost)
51+
port: Port to listen on (default: 8080)
52+
"""
53+
self.name = name
54+
self.host = host
55+
self.port = port
56+
self.mcp = FastMCP(name)
57+
self._tools: Dict[str, Callable] = {}
58+
self._server: Optional[Server] = None
59+
self._app: Optional[Starlette] = None
60+
61+
def handle_request(self, request_data: Dict[str, Any]) -> Dict[str, Any]:
62+
"""
63+
Handle incoming MCP requests.
64+
65+
This method can be overridden in subclasses to add custom request handling,
66+
such as latency tracking, authentication, or request modification.
67+
68+
Args:
69+
request_data: The incoming request data
70+
71+
Returns:
72+
Response data
73+
"""
74+
# Default implementation - can be overridden
75+
method = request_data.get('method', '')
76+
request_id = request_data.get('id', 'unknown')
77+
78+
logger.debug(f"Handling request {request_id}: {method}")
79+
80+
# Basic response structure
81+
response = {
82+
'id': request_id,
83+
'jsonrpc': '2.0',
84+
'result': {}
85+
}
86+
87+
return response
88+
89+
def add_tool(self, func: Callable, name: Optional[str] = None, description: Optional[str] = None):
90+
"""
91+
Add a tool to the MCP server.
92+
93+
Args:
94+
func: The function to expose as a tool
95+
name: Optional name for the tool (defaults to function name)
96+
description: Optional description for the tool
97+
"""
98+
tool_name = name or func.__name__
99+
100+
# Register with FastMCP
101+
if asyncio.iscoroutinefunction(func):
102+
# Already async
103+
self.mcp.tool(name=tool_name)(func)
104+
else:
105+
# Wrap sync function in async
106+
async def async_wrapper(*args, **kwargs):
107+
return func(*args, **kwargs)
108+
async_wrapper.__name__ = func.__name__
109+
async_wrapper.__doc__ = description or func.__doc__
110+
self.mcp.tool(name=tool_name)(async_wrapper)
111+
112+
self._tools[tool_name] = func
113+
logger.info(f"Added tool: {tool_name}")
114+
115+
def create_app(self, debug: bool = False) -> Starlette:
116+
"""
117+
Create a Starlette application for serving the MCP server.
118+
119+
Args:
120+
debug: Enable debug mode
121+
122+
Returns:
123+
Starlette application instance
124+
125+
Raises:
126+
RuntimeError: If the MCP server is not properly initialized
127+
"""
128+
if not self._server:
129+
if not hasattr(self.mcp, '_mcp_server'):
130+
raise RuntimeError("MCP server not properly initialized. Ensure FastMCP is correctly set up.")
131+
self._server = self.mcp._mcp_server
132+
133+
sse = SseServerTransport("/messages/")
134+
135+
async def handle_sse(request: Request) -> None:
136+
logger.debug(f"SSE connection from {request.client}")
137+
async with sse.connect_sse(
138+
request.scope,
139+
request.receive,
140+
request._send,
141+
) as (read_stream, write_stream):
142+
await self._server.run(
143+
read_stream,
144+
write_stream,
145+
self._server.create_initialization_options(),
146+
)
147+
148+
self._app = Starlette(
149+
debug=debug,
150+
routes=[
151+
Route("/sse", endpoint=handle_sse),
152+
Mount("/messages/", app=sse.handle_post_message),
153+
],
154+
)
155+
156+
return self._app
157+
158+
def start(self, debug: bool = False, **uvicorn_kwargs):
159+
"""
160+
Start the MCP server.
161+
162+
Args:
163+
debug: Enable debug mode
164+
**uvicorn_kwargs: Additional arguments to pass to uvicorn
165+
"""
166+
app = self.create_app(debug=debug)
167+
168+
print(f"Starting {self.name} MCP server on {self.host}:{self.port}")
169+
print(f"Available tools: {', '.join(self._tools.keys())}")
170+
print(f"SSE endpoint: http://{self.host}:{self.port}/sse")
171+
172+
uvicorn.run(app, host=self.host, port=self.port, **uvicorn_kwargs)
173+
174+
async def start_async(self, debug: bool = False):
175+
"""
176+
Start the MCP server asynchronously.
177+
178+
Args:
179+
debug: Enable debug mode
180+
"""
181+
app = self.create_app(debug=debug)
182+
183+
config = uvicorn.Config(app, host=self.host, port=self.port)
184+
server = uvicorn.Server(config)
185+
186+
print(f"Starting {self.name} MCP server on {self.host}:{self.port}")
187+
print(f"Available tools: {', '.join(self._tools.keys())}")
188+
189+
await server.serve()
190+
191+
def get_tools(self) -> List[str]:
192+
"""Get list of available tool names."""
193+
return list(self._tools.keys())
194+
195+
def get_endpoint(self) -> str:
196+
"""Get the SSE endpoint URL."""
197+
return f"http://{self.host}:{self.port}/sse"

examples/python/custom_tools/mcp_server_latency_example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"""
77

88
from praisonaiagents import Agent, PraisonAIAgents
9-
from praisonaiagents.mcp import HostedMCPServer
9+
from hosted_server import HostedMCPServer # Import from local file
1010
from latency_tracker_tool import tracker, get_latency_metrics
1111
import json
1212

src/praisonai-agents/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,4 @@ all = [
7373

7474
[tool.setuptools.packages.find]
7575
where = ["."]
76-
include = ["praisonaiagents*"]
76+
include = ["praisonaiagents*"]

0 commit comments

Comments
 (0)