Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,46 @@ Launch the webUI using
```
chainlit run start_webui.py
```

## MCP tool wrapper

EAA's MCP tool wrapper allows you to convert any tools that are subclasses of
`BaseTool` into an MCP tool and launch an MCP server offering these tools.
This allows you to use the tools in EAA with other MCP clients such as
Claude Code and Gemini CLI.

We will illustrate how an MCP server can be set up using a simple example. A
calculator tool, subclassing `BaseTool`, is created in
`src/eaa/tools/example_calculator.py`. To turn it into an MCP server, we
use `eaa.mcp.un_mcp_server_from_tools`. See `examples/mcp_calculator_server.py`
for an example.

After the server script is created, add it to the config JSON of your MCP client.
Refer to the documentations of the client on where this config file is located.
```json
{
"mcpServers": {
"calculator": {
"command": "python",
"args": ["path/to/mcp_calculator_server.py"]
}
}
}
```
If EAA is installed in a virtual environment, you will need to ask the MCP client
to activate the environment before launching the tool. Below is an example:
```json
{
"mcpServers": {
"calculator": {
"command": "bash",
"args": [
"-c",
"source /path/to/.venv/bin/activate && python path/to/mcp_calculator_server.py"
]
}
}
}
```
Now the MCP client should be able to run and connect to the MCP server and use the
tool.
77 changes: 77 additions & 0 deletions examples/mcp_calculator_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""
Example MCP Server using CalculatorTool.

This script demonstrates how to create and run an MCP server that exposes
BaseTool methods as MCP tools for use by AI applications like Cursor.

Usage:
python examples/mcp_calculator_server.py

The server will expose the calculator tool methods (add, subtract, multiply,
divide, get_history, clear_history) as MCP tools.
"""

import sys
import logging
from pathlib import Path

# Add the src directory to the Python path
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))

from eaa.tools.example_calculator import CalculatorTool
from eaa.mcp import run_mcp_server_from_tools

# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

logger = logging.getLogger(__name__)


def main():
"""Main function to run the MCP calculator server."""
try:
# Create the calculator tool
calculator = CalculatorTool()

logger.info("Created calculator tool with the following methods:")
for tool_dict in calculator.exposed_tools:
logger.info(f" - {tool_dict['name']}: {tool_dict['function'].__doc__.split('.')[0] if tool_dict['function'].__doc__ else 'No description'}")

# Create and run the MCP server
logger.info("Starting MCP server...")
run_mcp_server_from_tools(
tools=calculator,
server_name="Calculator MCP Server",
)

except KeyboardInterrupt:
logger.info("Server stopped by user")
except Exception as e:
logger.error(f"Error running MCP server: {e}")
raise


if __name__ == "__main__":
print("""
Calculator MCP Server Example
============================

This server exposes calculator operations as MCP tools:
- add(a, b): Add two numbers
- subtract(a, b): Subtract two numbers
- multiply(a, b): Multiply two numbers
- divide(a, b): Divide two numbers
- get_history(): Get calculation history
- clear_history(): Clear calculation history

Connect this server to your MCP client (like Cursor) to use these tools
in AI conversations.

Press Ctrl+C to stop the server.
""")

main()
7 changes: 7 additions & 0 deletions src/eaa/mcp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .server import MCPToolServer, create_mcp_server_from_tools, run_mcp_server_from_tools

__all__ = [
"MCPToolServer",
"create_mcp_server_from_tools",
"run_mcp_server_from_tools"
]
209 changes: 209 additions & 0 deletions src/eaa/mcp/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
"""
MCP Server component for exposing BaseTool subclasses as MCP tools.

This module provides functionality to create and run MCP servers that expose
methods from BaseTool subclasses as standardized MCP tools.
"""

import logging
from typing import Any, Dict, List, Optional, Union

try:
from mcp.server.fastmcp import FastMCP
except ImportError:
raise ImportError(
"The 'mcp' package is required to use the MCP server. "
"Install it with: pip install mcp"
)

from eaa.tools.base import BaseTool
from eaa.agents.base import generate_openai_tool_schema

logger = logging.getLogger(__name__)


class MCPToolServer:
"""
An MCP server that exposes BaseTool methods as MCP tools.

This class creates an MCP server that automatically converts BaseTool
subclasses into MCP-compatible tools, allowing them to be used by
MCP clients like Cursor or other AI applications.
"""

def __init__(
self,
name: str = "BaseTool MCP Server",
tools: Optional[List[BaseTool]] = None
):
"""
Initialize the MCP Tool Server.

Parameters
----------
name : str, optional
The name of the MCP server.
tools : List[BaseTool], optional
List of BaseTool instances to expose via MCP.
"""
self.name = name
self.mcp_server = FastMCP(name)
self._tool_instances: Dict[str, BaseTool] = {}
self._registered_tools: Dict[str, Dict[str, Any]] = {}

# Register tools if provided
if tools:
self.register_tools(tools)

def register_tools(self, tools: Union[BaseTool, List[BaseTool]]) -> None:
"""
Register BaseTool instances with the MCP server.

Parameters
----------
tools : Union[BaseTool, List[BaseTool]]
BaseTool instance(s) to register.
"""
if not isinstance(tools, (list, tuple)):
tools = [tools]

for tool in tools:
if not isinstance(tool, BaseTool):
raise ValueError(f"Tool must be a BaseTool instance, got {type(tool)}")

if not hasattr(tool, "exposed_tools") or not tool.exposed_tools:
raise ValueError(
f"BaseTool {tool.__class__.__name__} must have non-empty "
"`exposed_tools` attribute"
)

self._register_tool_instance(tool)

def _register_tool_instance(self, tool: BaseTool) -> None:
"""
Register all exposed tool methods of a BaseTool instance.

Parameters
----------
tool : BaseTool
The BaseTool instance to register.
"""
for tool_dict in tool.exposed_tools:
tool_name = tool_dict["name"]
tool_function = tool_dict["function"]

if tool_name in self._registered_tools:
raise ValueError(f"Tool '{tool_name}' is already registered")

# Store the tool instance for later method calls
self._tool_instances[tool_name] = tool
self._registered_tools[tool_name] = tool_dict

# Create the MCP tool
# This is equivalent to adding @self.mcp_server.tool()
# to the definition of the tool function.
self.mcp_server.tool()(tool_function)

def get_tool_schemas(self) -> List[Dict[str, Any]]:
"""
Get OpenAI-compatible tool schemas for all registered tools.

Note that the schemas returned are NOT what's used by the MCP server.
The MCP SDK creates the schemas itself using annotations and docstrings
in the tool functions. Only use this function for reference.

Returns
-------
List[Dict[str, Any]]
List of tool schemas.
"""
schemas = []
for tool_name, tool_dict in self._registered_tools.items():
schema = generate_openai_tool_schema(tool_name, tool_dict["function"])
schemas.append(schema)
return schemas

def list_tools(self) -> List[str]:
"""
List all registered tool names.

Returns
-------
List[str]
List of tool names.
"""
return list(self._registered_tools.keys())

def get_server(self) -> FastMCP:
"""
Get the underlying FastMCP server instance.

Returns
-------
FastMCP
The FastMCP server instance.
"""
return self.mcp_server

def run(self) -> None:
"""
Run the MCP server.

Parameters
----------
port : int, optional
The port to listen on.
**kwargs
Additional arguments passed to the FastMCP server.
"""
logger.info(f"Starting MCP server '{self.name}' with {len(self._registered_tools)} tools")
for tool_name in self.list_tools():
logger.info(f" - {tool_name}")

# Run the server
self.mcp_server.run()


def create_mcp_server_from_tools(
tools: Union[BaseTool, List[BaseTool]],
server_name: str = "BaseTool MCP Server"
) -> MCPToolServer:
"""
Convenience function to create an MCP server from BaseTool instances.

Parameters
----------
tools : Union[BaseTool, List[BaseTool]]
BaseTool instance(s) to expose via MCP.
server_name : str, optional
Name of the MCP server.

Returns
-------
MCPToolServer
Configured MCP server ready to run.
"""
server = MCPToolServer(name=server_name)
server.register_tools(tools)
return server


def run_mcp_server_from_tools(
tools: Union[BaseTool, List[BaseTool]],
server_name: str = "BaseTool MCP Server",
**server_kwargs
) -> None:
"""
Create and run an MCP server from BaseTool instances.

Parameters
----------
tools : Union[BaseTool, List[BaseTool]]
BaseTool instance(s) to expose via MCP.
server_name : str, optional
Name of the MCP server.
**server_kwargs
Additional arguments passed to the FastMCP.run method.
"""
server = create_mcp_server_from_tools(tools, server_name)
server.run(**server_kwargs)
Loading