mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-08 08:11:07 -10:00
- Add hosted_server.py to examples/python/custom_tools/ - Update documentation to clarify this is an example implementation - Keep starlette dependency as it's still used by Agent.launch() for MCP This makes HostedMCPServer an example implementation rather than a core package component. Co-authored-by: Mervin Praison <[email protected]>
197 lines
6.4 KiB
Python
197 lines
6.4 KiB
Python
"""
|
|
Hosted MCP Server implementation for PraisonAI Agents.
|
|
|
|
This module provides a base class for creating hosted MCP servers
|
|
that can handle requests and integrate with the MCP protocol.
|
|
|
|
Note: This is an example implementation. To use it, ensure you have installed:
|
|
pip install praisonaiagents[mcp] starlette>=0.27.0
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
from typing import Dict, Any, Optional, List, Callable
|
|
import json
|
|
|
|
try:
|
|
from mcp.server.fastmcp import FastMCP
|
|
from mcp.server import Server
|
|
from starlette.applications import Starlette
|
|
from starlette.requests import Request
|
|
from starlette.routing import Mount, Route
|
|
from mcp.server.sse import SseServerTransport
|
|
import uvicorn
|
|
except ImportError:
|
|
raise ImportError(
|
|
"MCP server dependencies not installed. "
|
|
"Please install with: pip install praisonaiagents[mcp] starlette>=0.27.0"
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class HostedMCPServer:
|
|
"""
|
|
Base class for creating hosted MCP servers.
|
|
|
|
This class provides a foundation for building MCP servers that can:
|
|
- Handle incoming requests
|
|
- Define custom tools
|
|
- Support SSE transport
|
|
- Be extended with custom functionality like latency tracking
|
|
"""
|
|
|
|
def __init__(self, name: str = "hosted-mcp-server", host: str = "localhost", port: int = 8080):
|
|
"""
|
|
Initialize the hosted MCP server.
|
|
|
|
Args:
|
|
name: Server name for identification
|
|
host: Host to bind to (default: localhost)
|
|
port: Port to listen on (default: 8080)
|
|
"""
|
|
self.name = name
|
|
self.host = host
|
|
self.port = port
|
|
self.mcp = FastMCP(name)
|
|
self._tools: Dict[str, Callable] = {}
|
|
self._server: Optional[Server] = None
|
|
self._app: Optional[Starlette] = None
|
|
|
|
def handle_request(self, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""
|
|
Handle incoming MCP requests.
|
|
|
|
This method can be overridden in subclasses to add custom request handling,
|
|
such as latency tracking, authentication, or request modification.
|
|
|
|
Args:
|
|
request_data: The incoming request data
|
|
|
|
Returns:
|
|
Response data
|
|
"""
|
|
# Default implementation - can be overridden
|
|
method = request_data.get('method', '')
|
|
request_id = request_data.get('id', 'unknown')
|
|
|
|
logger.debug(f"Handling request {request_id}: {method}")
|
|
|
|
# Basic response structure
|
|
response = {
|
|
'id': request_id,
|
|
'jsonrpc': '2.0',
|
|
'result': {}
|
|
}
|
|
|
|
return response
|
|
|
|
def add_tool(self, func: Callable, name: Optional[str] = None, description: Optional[str] = None):
|
|
"""
|
|
Add a tool to the MCP server.
|
|
|
|
Args:
|
|
func: The function to expose as a tool
|
|
name: Optional name for the tool (defaults to function name)
|
|
description: Optional description for the tool
|
|
"""
|
|
tool_name = name or func.__name__
|
|
|
|
# Register with FastMCP
|
|
if asyncio.iscoroutinefunction(func):
|
|
# Already async
|
|
self.mcp.tool(name=tool_name)(func)
|
|
else:
|
|
# Wrap sync function in async
|
|
async def async_wrapper(*args, **kwargs):
|
|
return func(*args, **kwargs)
|
|
async_wrapper.__name__ = func.__name__
|
|
async_wrapper.__doc__ = description or func.__doc__
|
|
self.mcp.tool(name=tool_name)(async_wrapper)
|
|
|
|
self._tools[tool_name] = func
|
|
logger.info(f"Added tool: {tool_name}")
|
|
|
|
def create_app(self, debug: bool = False) -> Starlette:
|
|
"""
|
|
Create a Starlette application for serving the MCP server.
|
|
|
|
Args:
|
|
debug: Enable debug mode
|
|
|
|
Returns:
|
|
Starlette application instance
|
|
|
|
Raises:
|
|
RuntimeError: If the MCP server is not properly initialized
|
|
"""
|
|
if not self._server:
|
|
if not hasattr(self.mcp, '_mcp_server'):
|
|
raise RuntimeError("MCP server not properly initialized. Ensure FastMCP is correctly set up.")
|
|
self._server = self.mcp._mcp_server
|
|
|
|
sse = SseServerTransport("/messages/")
|
|
|
|
async def handle_sse(request: Request) -> None:
|
|
logger.debug(f"SSE connection from {request.client}")
|
|
async with sse.connect_sse(
|
|
request.scope,
|
|
request.receive,
|
|
request._send,
|
|
) as (read_stream, write_stream):
|
|
await self._server.run(
|
|
read_stream,
|
|
write_stream,
|
|
self._server.create_initialization_options(),
|
|
)
|
|
|
|
self._app = Starlette(
|
|
debug=debug,
|
|
routes=[
|
|
Route("/sse", endpoint=handle_sse),
|
|
Mount("/messages/", app=sse.handle_post_message),
|
|
],
|
|
)
|
|
|
|
return self._app
|
|
|
|
def start(self, debug: bool = False, **uvicorn_kwargs):
|
|
"""
|
|
Start the MCP server.
|
|
|
|
Args:
|
|
debug: Enable debug mode
|
|
**uvicorn_kwargs: Additional arguments to pass to uvicorn
|
|
"""
|
|
app = self.create_app(debug=debug)
|
|
|
|
print(f"Starting {self.name} MCP server on {self.host}:{self.port}")
|
|
print(f"Available tools: {', '.join(self._tools.keys())}")
|
|
print(f"SSE endpoint: http://{self.host}:{self.port}/sse")
|
|
|
|
uvicorn.run(app, host=self.host, port=self.port, **uvicorn_kwargs)
|
|
|
|
async def start_async(self, debug: bool = False):
|
|
"""
|
|
Start the MCP server asynchronously.
|
|
|
|
Args:
|
|
debug: Enable debug mode
|
|
"""
|
|
app = self.create_app(debug=debug)
|
|
|
|
config = uvicorn.Config(app, host=self.host, port=self.port)
|
|
server = uvicorn.Server(config)
|
|
|
|
print(f"Starting {self.name} MCP server on {self.host}:{self.port}")
|
|
print(f"Available tools: {', '.join(self._tools.keys())}")
|
|
|
|
await server.serve()
|
|
|
|
def get_tools(self) -> List[str]:
|
|
"""Get list of available tool names."""
|
|
return list(self._tools.keys())
|
|
|
|
def get_endpoint(self) -> str:
|
|
"""Get the SSE endpoint URL."""
|
|
return f"http://{self.host}:{self.port}/sse" |