refactor: add Settings singleton pattern for config management
This commit is contained in:
26
AGENTS.md
26
AGENTS.md
@@ -8,11 +8,14 @@ TrytonMCP is an MCP (Model Context Protocol) server that enables LLMs to interac
|
|||||||
|
|
||||||
```
|
```
|
||||||
TrytonMCP/
|
TrytonMCP/
|
||||||
├── src/tryton-mcp/
|
├── src/
|
||||||
|
│ └── tryton_mcp/
|
||||||
│ ├── __init__.py
|
│ ├── __init__.py
|
||||||
|
│ ├── config.py # Settings singleton with TrytonSettings
|
||||||
│ └── server.py # Main MCP server with tool definitions
|
│ └── server.py # Main MCP server with tool definitions
|
||||||
├── test_rpc.py # Standalone RPC test script
|
├── test_rpc.py # Standalone RPC test script
|
||||||
└── pyproject.toml # Project dependencies
|
├── pyproject.toml # Project dependencies
|
||||||
|
└── .env.example # Environment variables template
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
@@ -23,16 +26,15 @@ TrytonMCP/
|
|||||||
|
|
||||||
### Core Components
|
### Core Components
|
||||||
|
|
||||||
1. **Client Connection** (`server.py:45-47, 59-61, 73-75`)
|
1. **Settings** (`config.py`) - Singleton pattern for configuration management
|
||||||
- Creates a new `Client` instance for each tool call
|
- Uses environment variables with defaults
|
||||||
- Credentials are hardcoded in `TRYTON_CREDENTIALS` dict
|
- Provides `to_dict()` and `get_client()` methods
|
||||||
|
- Validates required fields in `__post_init__`
|
||||||
|
|
||||||
2. **Tools** - Functions decorated with `@mcp.tool()`:
|
2. **Tools** (`server.py`) - Functions decorated with `@mcp.tool()`:
|
||||||
- `create_schedule` (line 32-54) - Creates appointments in Naliia module
|
- `create_schedule` - Creates appointments in Naliia module
|
||||||
- `find_service_centers` (line 57-69) - Searches service centers
|
- `find_service_centers` - Searches service centers
|
||||||
- `find_products_and_services` (line 71-83) - Searches salable products
|
- `find_products_and_services` - Searches salable products
|
||||||
|
|
||||||
3. **Context** (`server.py:20-24`) - Dataclass for managing client state (currently unused)
|
|
||||||
|
|
||||||
## Adding New Tools
|
## Adding New Tools
|
||||||
|
|
||||||
@@ -41,7 +43,7 @@ To add a new Tryton model as an MCP tool:
|
|||||||
```python
|
```python
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def your_tool_name(param1: int, param2: str) -> List[dict]:
|
def your_tool_name(param1: int, param2: str) -> List[dict]:
|
||||||
client = Client(**TRYTON_CREDENTIALS)
|
client = settings.get_client()
|
||||||
client.connect()
|
client.connect()
|
||||||
|
|
||||||
name = "model.your.model.method"
|
name = "model.your.model.method"
|
||||||
|
|||||||
42
src/tryton_mcp/config.py
Normal file
42
src/tryton_mcp/config.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import os
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
from sabatron_tryton_rpc_client.client import Client
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TrytonSettings:
|
||||||
|
hostname: str = field(
|
||||||
|
default_factory=lambda: os.environ.get("TRYTON_HOSTNAME", "localhost")
|
||||||
|
)
|
||||||
|
database: str = field(
|
||||||
|
default_factory=lambda: os.environ.get("TRYTON_DATABASE", "tryton")
|
||||||
|
)
|
||||||
|
username: str = field(
|
||||||
|
default_factory=lambda: os.environ.get("TRYTON_USERNAME", "admin")
|
||||||
|
)
|
||||||
|
password: str = field(default_factory=lambda: os.environ.get("TRYTON_PASSWORD", ""))
|
||||||
|
port: int = field(
|
||||||
|
default_factory=lambda: int(os.environ.get("TRYTON_PORT", "8000"))
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
return {
|
||||||
|
"hostname": self.hostname,
|
||||||
|
"database": self.database,
|
||||||
|
"username": self.username,
|
||||||
|
"password": self.password,
|
||||||
|
"port": self.port,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_client(self) -> Client:
|
||||||
|
return Client(**self.to_dict())
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
if not self.hostname:
|
||||||
|
raise ValueError("TRYTON_HOSTNAME is required")
|
||||||
|
if not self.database:
|
||||||
|
raise ValueError("TRYTON_DATABASE is required")
|
||||||
|
|
||||||
|
|
||||||
|
settings = TrytonSettings()
|
||||||
@@ -2,33 +2,12 @@
|
|||||||
MCP Server for Naliia Module
|
MCP Server for Naliia Module
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
from sabatron_tryton_rpc_client.client import Client
|
from sabatron_tryton_rpc_client.client import Client
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from fastmcp import FastMCP
|
from fastmcp import FastMCP
|
||||||
from typing import List, AsyncIterator
|
from typing import List
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
from tryton_mcp.config import settings
|
||||||
def get_tryton_credentials() -> dict:
|
|
||||||
return {
|
|
||||||
"hostname": os.environ.get("TRYTON_HOSTNAME", "localhost"),
|
|
||||||
"database": os.environ.get("TRYTON_DATABASE", "tryton"),
|
|
||||||
"username": os.environ.get("TRYTON_USERNAME", "admin"),
|
|
||||||
"password": os.environ.get("TRYTON_PASSWORD", ""),
|
|
||||||
"port": int(os.environ.get("TRYTON_PORT", 8000)),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
TRYTON_CREDENTIALS = get_tryton_credentials()
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AppContext:
|
|
||||||
"""Application context for the MCP server"""
|
|
||||||
|
|
||||||
client: Client
|
|
||||||
|
|
||||||
|
|
||||||
mcp = FastMCP("Tryton MCP Server")
|
mcp = FastMCP("Tryton MCP Server")
|
||||||
@@ -49,7 +28,7 @@ def create_schedule(
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
client = Client(**TRYTON_CREDENTIALS)
|
client = Client(**settings.to_dict())
|
||||||
|
|
||||||
client.connect()
|
client.connect()
|
||||||
name = "model.naliia.schedule.create"
|
name = "model.naliia.schedule.create"
|
||||||
@@ -61,7 +40,7 @@ def create_schedule(
|
|||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def find_service_centers():
|
def find_service_centers():
|
||||||
client = Client(**TRYTON_CREDENTIALS)
|
client = Client(**settings.to_dict())
|
||||||
|
|
||||||
client.connect()
|
client.connect()
|
||||||
|
|
||||||
@@ -74,7 +53,7 @@ def find_service_centers():
|
|||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def find_products_and_services():
|
def find_products_and_services():
|
||||||
client = Client(**TRYTON_CREDENTIALS)
|
client = Client(**settings.to_dict())
|
||||||
|
|
||||||
client.connect()
|
client.connect()
|
||||||
|
|
||||||
285
test_rpc.ipynb
Normal file
285
test_rpc.ipynb
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 1,
|
||||||
|
"id": "f1899961",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"from sabatron_tryton_rpc_client.client import Client\n",
|
||||||
|
"\n",
|
||||||
|
"client = Client(\n",
|
||||||
|
" hostname='dev.naliia.co',\n",
|
||||||
|
" database='capacitacion',\n",
|
||||||
|
" username='admin',\n",
|
||||||
|
" password='admin'\n",
|
||||||
|
")\n",
|
||||||
|
"\n",
|
||||||
|
"client.connect()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 3,
|
||||||
|
"id": "dfb1eac0",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"import datetime\n",
|
||||||
|
"\n",
|
||||||
|
"# [{}]\n",
|
||||||
|
"example_schedule = [{\n",
|
||||||
|
" 'professional': 6,\n",
|
||||||
|
" 'description': \"Bien Bonito Todo!\",\n",
|
||||||
|
" 'customer': 4,\n",
|
||||||
|
" 'date': datetime.datetime(2026, 1, 29, 14, 15),\n",
|
||||||
|
" 'service_center': 11\n",
|
||||||
|
"}]"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 4,
|
||||||
|
"id": "29e77a34",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"[{'id': 1, 'name': 'Spring Nails', 'code': '1'}]"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 4,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"name = 'model.party.party.read'\n",
|
||||||
|
"args = ([1], ['id', 'name', 'code'], {})\n",
|
||||||
|
"\n",
|
||||||
|
"client.call(name, args)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 5,
|
||||||
|
"id": "c0546146",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"[41]"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 5,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"name = \"model.naliia.schedule.create\"\n",
|
||||||
|
"args = [example_schedule, {}]\n",
|
||||||
|
"\n",
|
||||||
|
"client.call(name, args)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "08405a63",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"[{'id': 11,\n",
|
||||||
|
" 'name': 'Campo Amor',\n",
|
||||||
|
" 'address.': {'id': 1, 'street': 'Carrera 54 # 7-15, Campo Amor'}}]"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 135,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"name = \"model.naliia.service_center.search_read\"\n",
|
||||||
|
"args = [[[]], 0, None, None, ['name','address.street'], {}]\n",
|
||||||
|
"\n",
|
||||||
|
"client.call(name, args)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 139,
|
||||||
|
"id": "eaa93853",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"[{'description': None,\n",
|
||||||
|
" 'id': 1,\n",
|
||||||
|
" 'name': 'Acrilico Esculpido',\n",
|
||||||
|
" 'list_price': Decimal('98000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 2,\n",
|
||||||
|
" 'name': 'Base Ruber',\n",
|
||||||
|
" 'list_price': Decimal('68000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 3,\n",
|
||||||
|
" 'name': 'Dipping',\n",
|
||||||
|
" 'list_price': Decimal('63000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 4,\n",
|
||||||
|
" 'name': 'Forrado en acrilico',\n",
|
||||||
|
" 'list_price': Decimal('78000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 5,\n",
|
||||||
|
" 'name': 'Manos semipermanente',\n",
|
||||||
|
" 'list_price': Decimal('48000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 6,\n",
|
||||||
|
" 'name': 'Manos semipermanente con decorado',\n",
|
||||||
|
" 'list_price': Decimal('48000')},\n",
|
||||||
|
" {'description': '',\n",
|
||||||
|
" 'id': 28,\n",
|
||||||
|
" 'name': 'Manos semipermanentes sin decordo hombre',\n",
|
||||||
|
" 'list_price': Decimal('33000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 8,\n",
|
||||||
|
" 'name': 'Manos tradicionales',\n",
|
||||||
|
" 'list_price': Decimal('28000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 29,\n",
|
||||||
|
" 'name': 'Manos tradicionales',\n",
|
||||||
|
" 'list_price': Decimal('25000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 9,\n",
|
||||||
|
" 'name': 'Pestalas efecto natural',\n",
|
||||||
|
" 'list_price': Decimal('95000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 10,\n",
|
||||||
|
" 'name': 'Pestañas 2D',\n",
|
||||||
|
" 'list_price': Decimal('110000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 12,\n",
|
||||||
|
" 'name': 'Pestañas efecto pestañina',\n",
|
||||||
|
" 'list_price': Decimal('105000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 13,\n",
|
||||||
|
" 'name': 'Pestañas tecnológicas',\n",
|
||||||
|
" 'list_price': Decimal('120000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 14,\n",
|
||||||
|
" 'name': 'Pies semipermanentes',\n",
|
||||||
|
" 'list_price': Decimal('38000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 30,\n",
|
||||||
|
" 'name': 'Pies semipermanentes',\n",
|
||||||
|
" 'list_price': Decimal('36000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 15,\n",
|
||||||
|
" 'name': 'Pies tradicionales',\n",
|
||||||
|
" 'list_price': Decimal('30000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 31,\n",
|
||||||
|
" 'name': 'Pies tradicionales',\n",
|
||||||
|
" 'list_price': Decimal('30000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 16,\n",
|
||||||
|
" 'name': 'Poly gel',\n",
|
||||||
|
" 'list_price': Decimal('98000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 17,\n",
|
||||||
|
" 'name': 'Press on',\n",
|
||||||
|
" 'list_price': Decimal('88000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 18,\n",
|
||||||
|
" 'name': 'Retiro Forrado en acrílico',\n",
|
||||||
|
" 'list_price': Decimal('10000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 19,\n",
|
||||||
|
" 'name': 'Retiro Press on',\n",
|
||||||
|
" 'list_price': Decimal('20000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 33,\n",
|
||||||
|
" 'name': 'Retiro Press on',\n",
|
||||||
|
" 'list_price': Decimal('10000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 20,\n",
|
||||||
|
" 'name': 'Retiro semipermanente',\n",
|
||||||
|
" 'list_price': Decimal('10000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 32,\n",
|
||||||
|
" 'name': 'Retiro semipermanente',\n",
|
||||||
|
" 'list_price': Decimal('5000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 21,\n",
|
||||||
|
" 'name': 'Retoque Acrílico esculpido',\n",
|
||||||
|
" 'list_price': Decimal('78000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 22,\n",
|
||||||
|
" 'name': 'Retoque de pestañas 15 días',\n",
|
||||||
|
" 'list_price': Decimal('53000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 26,\n",
|
||||||
|
" 'name': 'Retoque de pestañas 20 días',\n",
|
||||||
|
" 'list_price': Decimal('63000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 23,\n",
|
||||||
|
" 'name': 'Retoque de pestañas tecnológicas y 2D 15 días',\n",
|
||||||
|
" 'list_price': Decimal('60000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 27,\n",
|
||||||
|
" 'name': 'Retoque de pestañas tecnológicas y 2D 20 días',\n",
|
||||||
|
" 'list_price': Decimal('70000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 24,\n",
|
||||||
|
" 'name': 'Retoque Forrado en acrílico',\n",
|
||||||
|
" 'list_price': Decimal('68000')},\n",
|
||||||
|
" {'description': None,\n",
|
||||||
|
" 'id': 25,\n",
|
||||||
|
" 'name': 'Retoque Press on',\n",
|
||||||
|
" 'list_price': Decimal('68000')}]"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 139,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"name = \"model.product.product.search_read\"\n",
|
||||||
|
"args = [[['active', '=', True], ['salable', '=', True]], 0, None, None, ['name','list_price', 'description'], {'company': 1}]\n",
|
||||||
|
"\n",
|
||||||
|
"client.call(name, args)\n",
|
||||||
|
"\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "TrytonMCP",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.14.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
17
test_rpc.py
17
test_rpc.py
@@ -1,14 +1,11 @@
|
|||||||
import os
|
import sys
|
||||||
from sabatron_tryton_rpc_client.client import Client
|
from pathlib import Path
|
||||||
import datetime
|
|
||||||
|
|
||||||
client = Client(
|
sys.path.insert(0, str(Path(__file__).parent / "src"))
|
||||||
hostname=os.environ.get("TRYTON_HOSTNAME", "localhost"),
|
|
||||||
database=os.environ.get("TRYTON_DATABASE", "tryton"),
|
from tryton_mcp.config import settings
|
||||||
username=os.environ.get("TRYTON_USERNAME", "admin"),
|
|
||||||
password=os.environ.get("TRYTON_PASSWORD", ""),
|
client = settings.get_client()
|
||||||
port=int(os.environ.get("TRYTON_PORT", 8000)),
|
|
||||||
)
|
|
||||||
|
|
||||||
client.connect()
|
client.connect()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user