Skip to content

Commit f7cb9bc

Browse files
committed
Add aden-tools: FastMCP server with core tools
1 parent e0fd3e6 commit f7cb9bc

38 files changed

+2679
-0
lines changed

aden-tools/BUILDING_TOOLS.md

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# Building Tools for Aden
2+
3+
This guide explains how to create new tools for the Aden agent framework.
4+
5+
## Quick Start Checklist
6+
7+
1. Create folder under `src/aden_tools/tools/<tool_name>/`
8+
2. Implement class extending `BaseTool` with `args_schema`
9+
3. Declare `env_vars` if your tool needs environment variables
10+
4. Implement `_run()` and optionally `_arun()` for async
11+
5. Add a `README.md` documenting your tool
12+
6. Export from `src/aden_tools/tools/__init__.py`
13+
7. Add tests in `tests/tools/`
14+
15+
## Tool Structure
16+
17+
Each tool lives in its own folder:
18+
19+
```
20+
src/aden_tools/tools/my_tool/
21+
├── __init__.py # Export the tool class
22+
├── my_tool.py # Tool implementation
23+
└── README.md # Documentation
24+
```
25+
26+
## Implementation Pattern
27+
28+
### 1. Define Input Schema
29+
30+
Use Pydantic to define your tool's input parameters:
31+
32+
```python
33+
from pydantic import BaseModel, Field
34+
35+
class MyToolSchema(BaseModel):
36+
"""Input schema for MyTool."""
37+
38+
query: str = Field(
39+
..., # Required
40+
description="The search query",
41+
min_length=1,
42+
max_length=500,
43+
)
44+
limit: int = Field(
45+
default=10,
46+
description="Maximum number of results",
47+
ge=1,
48+
le=100,
49+
)
50+
```
51+
52+
### 2. Implement the Tool Class
53+
54+
```python
55+
from aden_tools import BaseTool, EnvVar
56+
57+
class MyTool(BaseTool):
58+
name: str = "my_tool"
59+
description: str = (
60+
"Searches for items matching a query. "
61+
"Use this when you need to find specific information."
62+
)
63+
args_schema: type[BaseModel] = MyToolSchema
64+
65+
# Optional: declare required environment variables
66+
env_vars: list[EnvVar] = [
67+
EnvVar(
68+
name="MY_API_KEY",
69+
description="API key for the service",
70+
required=True,
71+
),
72+
]
73+
74+
def _run(self, query: str, limit: int = 10, **kwargs) -> str:
75+
"""Execute the tool."""
76+
try:
77+
# Your implementation here
78+
results = self._search(query, limit)
79+
return f"Found {len(results)} results"
80+
except Exception as e:
81+
return f"Error: {str(e)}"
82+
83+
async def _arun(self, query: str, limit: int = 10, **kwargs) -> str:
84+
"""Async version (optional)."""
85+
# Async implementation
86+
pass
87+
```
88+
89+
### 3. Export the Tool
90+
91+
In `src/aden_tools/tools/my_tool/__init__.py`:
92+
```python
93+
from .my_tool import MyTool, MyToolSchema
94+
95+
__all__ = ["MyTool", "MyToolSchema"]
96+
```
97+
98+
In `src/aden_tools/tools/__init__.py`:
99+
```python
100+
from .my_tool import MyTool, MyToolSchema
101+
102+
__all__ = [
103+
# ... existing tools
104+
"MyTool",
105+
"MyToolSchema",
106+
]
107+
```
108+
109+
## Required Attributes
110+
111+
| Attribute | Type | Description |
112+
|-----------|------|-------------|
113+
| `name` | str | Unique identifier (snake_case) |
114+
| `description` | str | What the tool does and when to use it |
115+
| `args_schema` | type[BaseModel] | Pydantic model for input validation |
116+
117+
## Optional Attributes
118+
119+
| Attribute | Type | Default | Description |
120+
|-----------|------|---------|-------------|
121+
| `env_vars` | list[EnvVar] | [] | Required environment variables |
122+
| `max_usage_count` | int \| None | None | Limit tool usage |
123+
| `result_as_answer` | bool | False | Use result as final answer |
124+
| `cache_function` | Callable | None | Caching logic |
125+
126+
## Best Practices
127+
128+
### Error Handling
129+
130+
Return helpful error messages instead of raising exceptions:
131+
132+
```python
133+
def _run(self, **kwargs) -> str:
134+
try:
135+
result = self._do_work()
136+
return result
137+
except SpecificError as e:
138+
return f"Failed to process: {str(e)}"
139+
except Exception as e:
140+
return f"Unexpected error: {str(e)}"
141+
```
142+
143+
### Environment Variables
144+
145+
Validate credentials early in `__init__` if needed:
146+
147+
```python
148+
def __init__(self, **kwargs):
149+
super().__init__(**kwargs)
150+
from aden_tools import validate_env_vars
151+
self._env = validate_env_vars(self.env_vars)
152+
```
153+
154+
### Return Values
155+
156+
- Return strings for simple results
157+
- Return dicts for structured data (will be JSON-serialized)
158+
- Keep output concise and deterministic
159+
160+
### Documentation
161+
162+
Every tool needs a `README.md` with:
163+
- Description and use cases
164+
- Usage examples
165+
- Argument table
166+
- Environment variables (if any)
167+
- Error handling notes
168+
169+
## Testing
170+
171+
Place tests in `tests/tools/test_my_tool.py`:
172+
173+
```python
174+
import pytest
175+
from aden_tools.tools import MyTool
176+
177+
def test_my_tool_basic():
178+
tool = MyTool()
179+
result = tool.run(query="test")
180+
assert "results" in result
181+
182+
def test_my_tool_validation():
183+
tool = MyTool()
184+
with pytest.raises(ValueError):
185+
tool.run(query="") # Empty query should fail
186+
```
187+
188+
Mock external APIs to keep tests fast and deterministic.
189+
190+
## Naming Conventions
191+
192+
- **Folder name**: `snake_case` (e.g., `file_read_tool`)
193+
- **Class name**: `PascalCase` ending in `Tool` (e.g., `FileReadTool`)
194+
- **Tool name attribute**: `snake_case` (e.g., `file_read`)

aden-tools/Dockerfile

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Aden Tools MCP Server
2+
# Exposes aden-tools via Model Context Protocol
3+
4+
FROM python:3.11-slim
5+
6+
WORKDIR /app
7+
8+
# Copy project files
9+
COPY pyproject.toml ./
10+
COPY README.md ./
11+
COPY src ./src
12+
COPY mcp_server.py ./
13+
14+
# Install package with all dependencies
15+
RUN pip install --no-cache-dir -e .
16+
17+
# Create non-root user for security
18+
RUN useradd -m -u 1001 appuser && chown -R appuser:appuser /app
19+
USER appuser
20+
21+
# Expose MCP server port
22+
EXPOSE 4001
23+
24+
# Health check - verify server is responding
25+
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
26+
CMD python -c "import httpx; httpx.get('http://localhost:4001/health').raise_for_status()" || exit 1
27+
28+
# Run MCP server with HTTP transport
29+
CMD ["python", "mcp_server.py"]

aden-tools/README.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Aden Tools
2+
3+
Tool library for the Aden agent framework. Provides a collection of tools that AI agents can use to interact with external systems, process data, and perform actions.
4+
5+
## Installation
6+
7+
```bash
8+
pip install -e aden-tools
9+
```
10+
11+
For development:
12+
```bash
13+
pip install -e "aden-tools[dev]"
14+
```
15+
16+
## Quick Start
17+
18+
```python
19+
from aden_tools import ExampleTool
20+
21+
# Create and use a tool
22+
tool = ExampleTool()
23+
result = tool.run(message="Hello World", uppercase=True)
24+
print(result) # "HELLO WORLD"
25+
```
26+
27+
## Creating Custom Tools
28+
29+
```python
30+
from pydantic import BaseModel, Field
31+
from aden_tools import BaseTool
32+
33+
class MyToolSchema(BaseModel):
34+
"""Input schema for MyTool."""
35+
query: str = Field(..., description="The search query")
36+
limit: int = Field(default=10, description="Max results")
37+
38+
class MyTool(BaseTool):
39+
name: str = "my_tool"
40+
description: str = "Searches for items matching the query"
41+
args_schema: type[BaseModel] = MyToolSchema
42+
43+
def _run(self, query: str, limit: int = 10, **kwargs) -> str:
44+
# Your tool logic here
45+
return f"Found {limit} results for: {query}"
46+
```
47+
48+
## Available Tools
49+
50+
| Tool | Description |
51+
|------|-------------|
52+
| `ExampleTool` | Template tool demonstrating the pattern |
53+
54+
## Project Structure
55+
56+
```
57+
aden-tools/
58+
├── src/aden_tools/
59+
│ ├── __init__.py # Main exports
60+
│ ├── base_tool.py # BaseTool class
61+
│ ├── tool_types.py # Type definitions
62+
│ ├── adapters/ # Framework adapters
63+
│ ├── utils/ # Utility functions
64+
│ └── tools/ # Tool implementations
65+
│ └── example_tool/ # Example tool
66+
├── tests/ # Test suite
67+
├── README.md
68+
├── BUILDING_TOOLS.md # Tool development guide
69+
└── pyproject.toml
70+
```
71+
72+
## Documentation
73+
74+
- [Building Tools Guide](BUILDING_TOOLS.md) - How to create new tools
75+
- Individual tool READMEs in `src/aden_tools/tools/*/README.md`
76+
77+
## License
78+
79+
MIT

aden-tools/mcp_server.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Aden Tools MCP Server
4+
5+
Exposes all aden-tools via Model Context Protocol using FastMCP.
6+
7+
Usage:
8+
# Run with HTTP transport (default, for Docker)
9+
python mcp_server.py
10+
11+
# Run with custom port
12+
python mcp_server.py --port 8001
13+
14+
# Run with STDIO transport (for local testing)
15+
python mcp_server.py --stdio
16+
17+
Environment Variables:
18+
MCP_PORT - Server port (default: 4001)
19+
BRAVE_SEARCH_API_KEY - Required for web_search tool
20+
"""
21+
import argparse
22+
import os
23+
24+
from fastmcp import FastMCP
25+
from starlette.requests import Request
26+
from starlette.responses import PlainTextResponse
27+
28+
mcp = FastMCP("aden-tools")
29+
30+
# Register all tools with the MCP server
31+
from aden_tools.tools import register_all_tools
32+
33+
tools = register_all_tools(mcp)
34+
print(f"[MCP] Registered {len(tools)} tools: {tools}")
35+
36+
37+
@mcp.custom_route("/health", methods=["GET"])
38+
async def health_check(request: Request) -> PlainTextResponse:
39+
"""Health check endpoint for container orchestration."""
40+
return PlainTextResponse("OK")
41+
42+
43+
@mcp.custom_route("/", methods=["GET"])
44+
async def index(request: Request) -> PlainTextResponse:
45+
"""Landing page for browser visits."""
46+
return PlainTextResponse("Welcome to the Hive MCP Server")
47+
48+
49+
def main() -> None:
50+
"""Entry point for the MCP server."""
51+
parser = argparse.ArgumentParser(description="Aden Tools MCP Server")
52+
parser.add_argument(
53+
"--port",
54+
type=int,
55+
default=int(os.getenv("MCP_PORT", "4001")),
56+
help="HTTP server port (default: 4001)",
57+
)
58+
parser.add_argument(
59+
"--host",
60+
default="0.0.0.0",
61+
help="HTTP server host (default: 0.0.0.0)",
62+
)
63+
parser.add_argument(
64+
"--stdio",
65+
action="store_true",
66+
help="Use STDIO transport instead of HTTP",
67+
)
68+
args = parser.parse_args()
69+
70+
if args.stdio:
71+
print("[MCP] Starting with STDIO transport")
72+
mcp.run(transport="stdio")
73+
else:
74+
print(f"[MCP] Starting HTTP server on {args.host}:{args.port}")
75+
mcp.run(transport="http", host=args.host, port=args.port)
76+
77+
78+
if __name__ == "__main__":
79+
main()

0 commit comments

Comments
 (0)