sienge-ecbiesek-mcp 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sienge-ecbiesek-mcp might be problematic. Click here for more details.
- sienge_ecbiesek_mcp-1.0.0.dist-info/METADATA +205 -0
- sienge_ecbiesek_mcp-1.0.0.dist-info/RECORD +9 -0
- sienge_ecbiesek_mcp-1.0.0.dist-info/WHEEL +5 -0
- sienge_ecbiesek_mcp-1.0.0.dist-info/entry_points.txt +2 -0
- sienge_ecbiesek_mcp-1.0.0.dist-info/licenses/LICENSE +21 -0
- sienge_ecbiesek_mcp-1.0.0.dist-info/top_level.txt +1 -0
- sienge_mcp/__init__.py +9 -0
- sienge_mcp/server.py +783 -0
- sienge_mcp/utils/logger.py +169 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sienge-ecbiesek-mcp
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Sienge ECBIESEK MCP Server - Model Context Protocol integration for Sienge API (ECBIESEK Company)
|
|
5
|
+
Author-email: ECBIESEK <ti@ecbiesek.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Moizas951/Sienge-mcp
|
|
8
|
+
Project-URL: Documentation, https://github.com/Moizas951/Sienge-mcp#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/Moizas951/Sienge-mcp.git
|
|
10
|
+
Project-URL: Issues, https://github.com/Moizas951/Sienge-mcp/issues
|
|
11
|
+
Keywords: sienge,mcp,claude,api,construction,ecbiesek
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: fastmcp>=0.1.0
|
|
27
|
+
Requires-Dist: httpx>=0.25.0
|
|
28
|
+
Requires-Dist: pydantic>=2.0.0
|
|
29
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
32
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
33
|
+
Requires-Dist: flake8>=6.0.0; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# 🏗️ Sienge ECBIESEK MCP Server
|
|
37
|
+
|
|
38
|
+
[](https://badge.fury.io/py/sienge-ecbiesek-mcp)
|
|
39
|
+
[](https://www.python.org/downloads/)
|
|
40
|
+
|
|
41
|
+
**Sienge ECBIESEK MCP Server** - Servidor MCP personalizado para a empresa ECBIESEK integrar com a API do Sienge através do Claude Desktop.
|
|
42
|
+
|
|
43
|
+
## 🚀 Instalação Rápida (ECBIESEK)
|
|
44
|
+
|
|
45
|
+
### Usando pipx (Recomendado)
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Executar diretamente sem instalação
|
|
49
|
+
pipx run sienge-ecbiesek-mcp
|
|
50
|
+
|
|
51
|
+
# Ou instalar permanentemente
|
|
52
|
+
pipx install sienge-ecbiesek-mcp
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Usando pip
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install sienge-ecbiesek-mcp
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## ⚙️ Configuração no Claude Desktop (ECBIESEK)
|
|
62
|
+
|
|
63
|
+
Adicione a seguinte configuração ao seu arquivo `claude_desktop_config.json`:
|
|
64
|
+
|
|
65
|
+
### Para pipx (Recomendado):
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"mcpServers": {
|
|
69
|
+
"SiengeECBIESEK": {
|
|
70
|
+
"command": "pipx",
|
|
71
|
+
"args": [
|
|
72
|
+
"run",
|
|
73
|
+
"sienge-ecbiesek-mcp@latest"
|
|
74
|
+
],
|
|
75
|
+
"env": {
|
|
76
|
+
"SIENGE_BASE_URL": "https://api.sienge.com.br",
|
|
77
|
+
"SIENGE_SUBDOMAIN": "ecbiesek",
|
|
78
|
+
"SIENGE_USERNAME": "seu-usuario-ecbiesek",
|
|
79
|
+
"SIENGE_PASSWORD": "sua-senha-ecbiesek"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Para pip (instalação global):
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"mcpServers": {
|
|
90
|
+
"SiengeECBIESEK": {
|
|
91
|
+
"command": "sienge-ecbiesek-mcp",
|
|
92
|
+
"env": {
|
|
93
|
+
"SIENGE_BASE_URL": "https://api.sienge.com.br",
|
|
94
|
+
"SIENGE_SUBDOMAIN": "ecbiesek",
|
|
95
|
+
"SIENGE_USERNAME": "seu-usuario-ecbiesek",
|
|
96
|
+
"SIENGE_PASSWORD": "sua-senha-ecbiesek"
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## 🔐 Variáveis de Ambiente (ECBIESEK)
|
|
104
|
+
|
|
105
|
+
### Autenticação Basic Auth (Configuração ECBIESEK)
|
|
106
|
+
```bash
|
|
107
|
+
SIENGE_USERNAME=usuario_ecbiesek
|
|
108
|
+
SIENGE_PASSWORD=senha_ecbiesek
|
|
109
|
+
SIENGE_SUBDOMAIN=ecbiesek
|
|
110
|
+
SIENGE_BASE_URL=https://api.sienge.com.br
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Autenticação Bearer Token (Alternativa)
|
|
114
|
+
```bash
|
|
115
|
+
SIENGE_API_KEY=token_api_ecbiesek
|
|
116
|
+
SIENGE_BASE_URL=https://api.sienge.com.br
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## 🛠️ Ferramentas Disponíveis
|
|
120
|
+
|
|
121
|
+
O servidor ECBIESEK fornece mais de 30 ferramentas para interagir com a API do Sienge:
|
|
122
|
+
|
|
123
|
+
### 📊 Relatórios e Listagens
|
|
124
|
+
- `listar_projetos` - Lista todos os projetos ECBIESEK
|
|
125
|
+
- `listar_contas` - Lista contas a pagar/receber
|
|
126
|
+
- `listar_centros_custo` - Lista centros de custo
|
|
127
|
+
- `listar_fornecedores` - Lista fornecedores
|
|
128
|
+
- `listar_colaboradores` - Lista colaboradores ECBIESEK
|
|
129
|
+
|
|
130
|
+
### 🏗️ Gestão de Projetos
|
|
131
|
+
- `obter_projeto` - Obtém detalhes de um projeto específico
|
|
132
|
+
- `listar_etapas_projeto` - Lista etapas de um projeto
|
|
133
|
+
- `listar_medicoes_projeto` - Lista medições de um projeto
|
|
134
|
+
|
|
135
|
+
### 💰 Financeiro
|
|
136
|
+
- `criar_conta_pagar` - Cria conta a pagar
|
|
137
|
+
- `atualizar_conta` - Atualiza conta existente
|
|
138
|
+
- `obter_detalhes_conta` - Obtém detalhes de uma conta
|
|
139
|
+
|
|
140
|
+
### 👥 Recursos Humanos ECBIESEK
|
|
141
|
+
- `obter_colaborador` - Obtém detalhes de um colaborador
|
|
142
|
+
- `listar_departamentos` - Lista departamentos
|
|
143
|
+
- `listar_cargos` - Lista cargos
|
|
144
|
+
|
|
145
|
+
### 📦 Estoque e Suprimentos
|
|
146
|
+
- `listar_produtos` - Lista produtos
|
|
147
|
+
- `obter_produto` - Obtém detalhes de um produto
|
|
148
|
+
- `listar_grupos_produto` - Lista grupos de produtos
|
|
149
|
+
|
|
150
|
+
### ⚙️ Configurações
|
|
151
|
+
- `obter_configuracoes_sistema` - Obtém configurações do sistema
|
|
152
|
+
- `listar_moedas` - Lista moedas disponíveis
|
|
153
|
+
- `obter_info_autenticacao` - Verifica status da autenticação
|
|
154
|
+
|
|
155
|
+
## 📝 Exemplos de Uso (ECBIESEK)
|
|
156
|
+
|
|
157
|
+
### Listar Projetos
|
|
158
|
+
```
|
|
159
|
+
Claude: Liste todos os projetos ativos da ECBIESEK no Sienge.
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Criar Conta a Pagar
|
|
163
|
+
```
|
|
164
|
+
Claude: Crie uma conta a pagar para o fornecedor XYZ no valor de R$ 1.500,00 com vencimento em 30 dias.
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Obter Relatório Financeiro
|
|
168
|
+
```
|
|
169
|
+
Claude: Me mostre um resumo das contas a pagar em aberto dos projetos da ECBIESEK.
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## 🏢 Sobre a ECBIESEK
|
|
173
|
+
|
|
174
|
+
Este MCP Server foi desenvolvido especificamente para atender às necessidades da empresa ECBIESEK na integração com o sistema Sienge.
|
|
175
|
+
|
|
176
|
+
## 📋 Requisitos
|
|
177
|
+
|
|
178
|
+
- Python 3.9 ou superior
|
|
179
|
+
- Acesso à API do Sienge com credenciais ECBIESEK
|
|
180
|
+
- Claude Desktop instalado
|
|
181
|
+
|
|
182
|
+
## 🐛 Resolução de Problemas
|
|
183
|
+
|
|
184
|
+
### Erro de Autenticação
|
|
185
|
+
- Verifique se as credenciais ECBIESEK estão corretas
|
|
186
|
+
- Confirme se o subdomínio "ecbiesek" está correto
|
|
187
|
+
- Teste as credenciais diretamente na API do Sienge
|
|
188
|
+
|
|
189
|
+
### Timeout de Requisições
|
|
190
|
+
- Aumente o valor de `REQUEST_TIMEOUT`
|
|
191
|
+
- Verifique a conectividade com a API do Sienge
|
|
192
|
+
|
|
193
|
+
## 📄 Licença
|
|
194
|
+
|
|
195
|
+
MIT License - Desenvolvido para ECBIESEK
|
|
196
|
+
|
|
197
|
+
## 📞 Suporte ECBIESEK
|
|
198
|
+
|
|
199
|
+
- 📧 Email: ti@ecbiesek.com
|
|
200
|
+
- 🏢 Empresa: ECBIESEK
|
|
201
|
+
- 📖 Documentação interna
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
**⭐ Sienge ECBIESEK MCP Server - Solução personalizada para integração Sienge + Claude Desktop**
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
sienge_ecbiesek_mcp-1.0.0.dist-info/licenses/LICENSE,sha256=leWD46QLXsQ43M8fE_KgOo5Sf0YB9_X8EVqGdV0Dsc0,1101
|
|
2
|
+
sienge_mcp/__init__.py,sha256=Pjl4hgBCWhVJ_BBZXaP7SuZfH7Z1JWZbSs8MV5sUle8,287
|
|
3
|
+
sienge_mcp/server.py,sha256=-7JO_QGvI2Bsw2Fg3kcAmVoLLVoEsffZiHwjw3jLPzc,27107
|
|
4
|
+
sienge_mcp/utils/logger.py,sha256=bqU0GDsQXE9TaKOq5_6S2L8bh_Nas-EuYNDE3fzlPWg,5880
|
|
5
|
+
sienge_ecbiesek_mcp-1.0.0.dist-info/METADATA,sha256=kUMzLjwbt_ACAuUJI5M-n6PIuewJP6cwd4ZqX_WfFyI,6318
|
|
6
|
+
sienge_ecbiesek_mcp-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
+
sienge_ecbiesek_mcp-1.0.0.dist-info/entry_points.txt,sha256=jxEu6gvTw3ci0mjDfqbi0rBLRpeuscwwRk9-H-UOnO8,63
|
|
8
|
+
sienge_ecbiesek_mcp-1.0.0.dist-info/top_level.txt,sha256=FCvuhB9JQPKGY0Q8aKoVc7akqG5htoJyfj-eJvVUmWM,11
|
|
9
|
+
sienge_ecbiesek_mcp-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Sienge MCP Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sienge_mcp
|
sienge_mcp/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sienge MCP Server Package
|
|
3
|
+
Model Context Protocol integration for Sienge API
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
__version__ = "1.0.0"
|
|
7
|
+
__author__ = "Sienge MCP Contributors"
|
|
8
|
+
__email__ = "contributors@sienge-mcp.com"
|
|
9
|
+
__description__ = "Sienge MCP Server - Model Context Protocol integration for Sienge API"
|
sienge_mcp/server.py
ADDED
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
SIENGE MCP COMPLETO - FastMCP com Autenticação Flexível
|
|
4
|
+
Suporta Bearer Token e Basic Auth
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from fastmcp import FastMCP
|
|
8
|
+
import httpx
|
|
9
|
+
import asyncio
|
|
10
|
+
from typing import Dict, List, Optional, Any
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
from dotenv import load_dotenv
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
import base64
|
|
16
|
+
|
|
17
|
+
# Carrega as variáveis de ambiente
|
|
18
|
+
load_dotenv()
|
|
19
|
+
|
|
20
|
+
mcp = FastMCP("Sienge API Integration 🏗️")
|
|
21
|
+
|
|
22
|
+
# Configurações da API do Sienge
|
|
23
|
+
SIENGE_BASE_URL = os.getenv("SIENGE_BASE_URL", "https://api.sienge.com.br")
|
|
24
|
+
SIENGE_SUBDOMAIN = os.getenv("SIENGE_SUBDOMAIN", "")
|
|
25
|
+
SIENGE_USERNAME = os.getenv("SIENGE_USERNAME", "")
|
|
26
|
+
SIENGE_PASSWORD = os.getenv("SIENGE_PASSWORD", "")
|
|
27
|
+
SIENGE_API_KEY = os.getenv("SIENGE_API_KEY", "")
|
|
28
|
+
REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", "30"))
|
|
29
|
+
|
|
30
|
+
class SiengeAPIError(Exception):
|
|
31
|
+
"""Exceção customizada para erros da API do Sienge"""
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
async def make_sienge_request(method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None) -> Dict:
|
|
35
|
+
"""
|
|
36
|
+
Função auxiliar para fazer requisições à API do Sienge
|
|
37
|
+
Suporta tanto Bearer Token quanto Basic Auth
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
|
|
41
|
+
headers = {
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
"Accept": "application/json"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Configurar autenticação e URL
|
|
47
|
+
auth = None
|
|
48
|
+
|
|
49
|
+
if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
|
|
50
|
+
# Bearer Token (Recomendado)
|
|
51
|
+
headers["Authorization"] = f"Bearer {SIENGE_API_KEY}"
|
|
52
|
+
url = f"{SIENGE_BASE_URL}/{SIENGE_SUBDOMAIN}/public/api/v1{endpoint}"
|
|
53
|
+
elif SIENGE_USERNAME and SIENGE_PASSWORD:
|
|
54
|
+
# Basic Auth usando httpx.BasicAuth
|
|
55
|
+
auth = httpx.BasicAuth(SIENGE_USERNAME, SIENGE_PASSWORD)
|
|
56
|
+
url = f"{SIENGE_BASE_URL}/{SIENGE_SUBDOMAIN}/public/api/v1{endpoint}"
|
|
57
|
+
else:
|
|
58
|
+
return {
|
|
59
|
+
"success": False,
|
|
60
|
+
"error": "No Authentication",
|
|
61
|
+
"message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
response = await client.request(
|
|
65
|
+
method=method,
|
|
66
|
+
url=url,
|
|
67
|
+
headers=headers,
|
|
68
|
+
params=params,
|
|
69
|
+
json=json_data,
|
|
70
|
+
auth=auth
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if response.status_code in [200, 201]:
|
|
74
|
+
try:
|
|
75
|
+
return {
|
|
76
|
+
"success": True,
|
|
77
|
+
"data": response.json(),
|
|
78
|
+
"status_code": response.status_code
|
|
79
|
+
}
|
|
80
|
+
except:
|
|
81
|
+
return {
|
|
82
|
+
"success": True,
|
|
83
|
+
"data": {"message": "Success"},
|
|
84
|
+
"status_code": response.status_code
|
|
85
|
+
}
|
|
86
|
+
else:
|
|
87
|
+
return {
|
|
88
|
+
"success": False,
|
|
89
|
+
"error": f"HTTP {response.status_code}",
|
|
90
|
+
"message": response.text,
|
|
91
|
+
"status_code": response.status_code
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
except httpx.TimeoutException:
|
|
95
|
+
return {
|
|
96
|
+
"success": False,
|
|
97
|
+
"error": "Timeout",
|
|
98
|
+
"message": f"A requisição excedeu o tempo limite de {REQUEST_TIMEOUT}s"
|
|
99
|
+
}
|
|
100
|
+
except Exception as e:
|
|
101
|
+
return {
|
|
102
|
+
"success": False,
|
|
103
|
+
"error": str(e),
|
|
104
|
+
"message": f"Erro na requisição: {str(e)}"
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# ============ CONEXÃO E TESTE ============
|
|
108
|
+
|
|
109
|
+
@mcp.tool
|
|
110
|
+
async def test_sienge_connection() -> Dict:
|
|
111
|
+
"""Testa a conexão com a API do Sienge"""
|
|
112
|
+
try:
|
|
113
|
+
# Tentar endpoint mais simples primeiro
|
|
114
|
+
result = await make_sienge_request("GET", "/customer-types")
|
|
115
|
+
|
|
116
|
+
if result["success"]:
|
|
117
|
+
auth_method = "Bearer Token" if SIENGE_API_KEY else "Basic Auth"
|
|
118
|
+
return {
|
|
119
|
+
"success": True,
|
|
120
|
+
"message": "✅ Conexão com API do Sienge estabelecida com sucesso!",
|
|
121
|
+
"api_status": "Online",
|
|
122
|
+
"auth_method": auth_method,
|
|
123
|
+
"timestamp": datetime.now().isoformat()
|
|
124
|
+
}
|
|
125
|
+
else:
|
|
126
|
+
return {
|
|
127
|
+
"success": False,
|
|
128
|
+
"message": "❌ Falha ao conectar com API do Sienge",
|
|
129
|
+
"error": result.get("error"),
|
|
130
|
+
"details": result.get("message"),
|
|
131
|
+
"timestamp": datetime.now().isoformat()
|
|
132
|
+
}
|
|
133
|
+
except Exception as e:
|
|
134
|
+
return {
|
|
135
|
+
"success": False,
|
|
136
|
+
"message": "❌ Erro ao testar conexão",
|
|
137
|
+
"error": str(e),
|
|
138
|
+
"timestamp": datetime.now().isoformat()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# ============ CLIENTES ============
|
|
142
|
+
|
|
143
|
+
@mcp.tool
|
|
144
|
+
async def get_sienge_customers(limit: Optional[int] = 50, offset: Optional[int] = 0, search: Optional[str] = None, customer_type_id: Optional[str] = None) -> Dict:
|
|
145
|
+
"""
|
|
146
|
+
Busca clientes no Sienge com filtros
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
limit: Máximo de registros (padrão: 50)
|
|
150
|
+
offset: Pular registros (padrão: 0)
|
|
151
|
+
search: Buscar por nome ou documento
|
|
152
|
+
customer_type_id: Filtrar por tipo de cliente
|
|
153
|
+
"""
|
|
154
|
+
params = {"limit": min(limit or 50, 200), "offset": offset or 0}
|
|
155
|
+
|
|
156
|
+
if search:
|
|
157
|
+
params["search"] = search
|
|
158
|
+
if customer_type_id:
|
|
159
|
+
params["customer_type_id"] = customer_type_id
|
|
160
|
+
|
|
161
|
+
result = await make_sienge_request("GET", "/customers", params=params)
|
|
162
|
+
|
|
163
|
+
if result["success"]:
|
|
164
|
+
data = result["data"]
|
|
165
|
+
customers = data.get("results", []) if isinstance(data, dict) else data
|
|
166
|
+
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
167
|
+
total_count = metadata.get("count", len(customers))
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
"success": True,
|
|
171
|
+
"message": f"✅ Encontrados {len(customers)} clientes (total: {total_count})",
|
|
172
|
+
"customers": customers,
|
|
173
|
+
"count": len(customers),
|
|
174
|
+
"filters_applied": params
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
"success": False,
|
|
179
|
+
"message": "❌ Erro ao buscar clientes",
|
|
180
|
+
"error": result.get("error"),
|
|
181
|
+
"details": result.get("message")
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
@mcp.tool
|
|
185
|
+
async def get_sienge_customer_types() -> Dict:
|
|
186
|
+
"""Lista tipos de clientes disponíveis"""
|
|
187
|
+
result = await make_sienge_request("GET", "/customer-types")
|
|
188
|
+
|
|
189
|
+
if result["success"]:
|
|
190
|
+
data = result["data"]
|
|
191
|
+
customer_types = data.get("results", []) if isinstance(data, dict) else data
|
|
192
|
+
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
193
|
+
total_count = metadata.get("count", len(customer_types))
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
"success": True,
|
|
197
|
+
"message": f"✅ Encontrados {len(customer_types)} tipos de clientes (total: {total_count})",
|
|
198
|
+
"customer_types": customer_types,
|
|
199
|
+
"count": len(customer_types)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
"success": False,
|
|
204
|
+
"message": "❌ Erro ao buscar tipos de clientes",
|
|
205
|
+
"error": result.get("error"),
|
|
206
|
+
"details": result.get("message")
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
# ============ CREDORES ============
|
|
210
|
+
|
|
211
|
+
@mcp.tool
|
|
212
|
+
async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int] = 0, search: Optional[str] = None) -> Dict:
|
|
213
|
+
"""
|
|
214
|
+
Busca credores/fornecedores
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
limit: Máximo de registros (padrão: 50)
|
|
218
|
+
offset: Pular registros (padrão: 0)
|
|
219
|
+
search: Buscar por nome
|
|
220
|
+
"""
|
|
221
|
+
params = {"limit": min(limit or 50, 200), "offset": offset or 0}
|
|
222
|
+
if search:
|
|
223
|
+
params["search"] = search
|
|
224
|
+
|
|
225
|
+
result = await make_sienge_request("GET", "/creditors", params=params)
|
|
226
|
+
|
|
227
|
+
if result["success"]:
|
|
228
|
+
data = result["data"]
|
|
229
|
+
creditors = data.get("results", []) if isinstance(data, dict) else data
|
|
230
|
+
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
231
|
+
total_count = metadata.get("count", len(creditors))
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
"success": True,
|
|
235
|
+
"message": f"✅ Encontrados {len(creditors)} credores (total: {total_count})",
|
|
236
|
+
"creditors": creditors,
|
|
237
|
+
"count": len(creditors)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
"success": False,
|
|
242
|
+
"message": "❌ Erro ao buscar credores",
|
|
243
|
+
"error": result.get("error"),
|
|
244
|
+
"details": result.get("message")
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
@mcp.tool
|
|
248
|
+
async def get_sienge_creditor_bank_info(creditor_id: str) -> Dict:
|
|
249
|
+
"""
|
|
250
|
+
Consulta informações bancárias de um credor
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
creditor_id: ID do credor (obrigatório)
|
|
254
|
+
"""
|
|
255
|
+
result = await make_sienge_request("GET", f"/creditors/{creditor_id}/bank-informations")
|
|
256
|
+
|
|
257
|
+
if result["success"]:
|
|
258
|
+
return {
|
|
259
|
+
"success": True,
|
|
260
|
+
"message": f"✅ Informações bancárias do credor {creditor_id}",
|
|
261
|
+
"creditor_id": creditor_id,
|
|
262
|
+
"bank_info": result["data"]
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
"success": False,
|
|
267
|
+
"message": f"❌ Erro ao buscar info bancária do credor {creditor_id}",
|
|
268
|
+
"error": result.get("error"),
|
|
269
|
+
"details": result.get("message")
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
# ============ FINANCEIRO ============
|
|
273
|
+
|
|
274
|
+
@mcp.tool
|
|
275
|
+
async def get_sienge_accounts_receivable(customer_id: Optional[str] = None, due_date_from: Optional[str] = None,
|
|
276
|
+
due_date_to: Optional[str] = None, status: Optional[str] = None, limit: Optional[int] = 50) -> Dict:
|
|
277
|
+
"""
|
|
278
|
+
Consulta títulos a receber
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
customer_id: ID do cliente
|
|
282
|
+
due_date_from: Data inicial (YYYY-MM-DD)
|
|
283
|
+
due_date_to: Data final (YYYY-MM-DD)
|
|
284
|
+
status: Status (open, paid, overdue)
|
|
285
|
+
limit: Máximo de registros
|
|
286
|
+
"""
|
|
287
|
+
params = {"limit": min(limit or 50, 200)}
|
|
288
|
+
|
|
289
|
+
if customer_id:
|
|
290
|
+
params["customer_id"] = customer_id
|
|
291
|
+
if due_date_from:
|
|
292
|
+
params["due_date_from"] = due_date_from
|
|
293
|
+
if due_date_to:
|
|
294
|
+
params["due_date_to"] = due_date_to
|
|
295
|
+
if status:
|
|
296
|
+
params["status"] = status
|
|
297
|
+
|
|
298
|
+
result = await make_sienge_request("GET", "/accounts-receivable", params=params)
|
|
299
|
+
|
|
300
|
+
if result["success"]:
|
|
301
|
+
data = result["data"]
|
|
302
|
+
receivables = data.get("results", []) if isinstance(data, dict) else data
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
"success": True,
|
|
306
|
+
"message": f"✅ Encontrados {len(receivables)} títulos a receber",
|
|
307
|
+
"receivables": receivables,
|
|
308
|
+
"count": len(receivables),
|
|
309
|
+
"filters": params
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
"success": False,
|
|
314
|
+
"message": "❌ Erro ao buscar títulos a receber",
|
|
315
|
+
"error": result.get("error"),
|
|
316
|
+
"details": result.get("message")
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
@mcp.tool
|
|
320
|
+
async def get_sienge_bills(start_date: Optional[str] = None, end_date: Optional[str] = None,
|
|
321
|
+
creditor_id: Optional[str] = None, status: Optional[str] = None,
|
|
322
|
+
limit: Optional[int] = 50) -> Dict:
|
|
323
|
+
"""
|
|
324
|
+
Consulta títulos a pagar (contas a pagar) - REQUER startDate obrigatório
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
start_date: Data inicial obrigatória (YYYY-MM-DD) - padrão últimos 30 dias
|
|
328
|
+
end_date: Data final (YYYY-MM-DD) - padrão hoje
|
|
329
|
+
creditor_id: ID do credor
|
|
330
|
+
status: Status do título (ex: open, paid, cancelled)
|
|
331
|
+
limit: Máximo de registros (padrão: 50, máx: 200)
|
|
332
|
+
"""
|
|
333
|
+
from datetime import datetime, timedelta
|
|
334
|
+
|
|
335
|
+
# Se start_date não fornecido, usar últimos 30 dias
|
|
336
|
+
if not start_date:
|
|
337
|
+
start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
|
|
338
|
+
|
|
339
|
+
# Se end_date não fornecido, usar hoje
|
|
340
|
+
if not end_date:
|
|
341
|
+
end_date = datetime.now().strftime('%Y-%m-%d')
|
|
342
|
+
|
|
343
|
+
# Parâmetros obrigatórios
|
|
344
|
+
params = {
|
|
345
|
+
"startDate": start_date, # OBRIGATÓRIO pela API
|
|
346
|
+
"endDate": end_date,
|
|
347
|
+
"limit": min(limit or 50, 200)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
# Parâmetros opcionais
|
|
351
|
+
if creditor_id:
|
|
352
|
+
params["creditor_id"] = creditor_id
|
|
353
|
+
if status:
|
|
354
|
+
params["status"] = status
|
|
355
|
+
|
|
356
|
+
result = await make_sienge_request("GET", "/bills", params=params)
|
|
357
|
+
|
|
358
|
+
if result["success"]:
|
|
359
|
+
data = result["data"]
|
|
360
|
+
bills = data.get("results", []) if isinstance(data, dict) else data
|
|
361
|
+
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
362
|
+
total_count = metadata.get("count", len(bills))
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
"success": True,
|
|
366
|
+
"message": f"✅ Encontrados {len(bills)} títulos a pagar (total: {total_count}) - período: {start_date} a {end_date}",
|
|
367
|
+
"bills": bills,
|
|
368
|
+
"count": len(bills),
|
|
369
|
+
"total_count": total_count,
|
|
370
|
+
"period": {"start_date": start_date, "end_date": end_date},
|
|
371
|
+
"filters": params
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
"success": False,
|
|
376
|
+
"message": "❌ Erro ao buscar títulos a pagar",
|
|
377
|
+
"error": result.get("error"),
|
|
378
|
+
"details": result.get("message")
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
# ============ COMPRAS ============
|
|
382
|
+
|
|
383
|
+
@mcp.tool
|
|
384
|
+
async def get_sienge_purchase_orders(purchase_order_id: Optional[str] = None, status: Optional[str] = None,
|
|
385
|
+
date_from: Optional[str] = None, limit: Optional[int] = 50) -> Dict:
|
|
386
|
+
"""
|
|
387
|
+
Consulta pedidos de compra
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
purchase_order_id: ID específico do pedido
|
|
391
|
+
status: Status do pedido
|
|
392
|
+
date_from: Data inicial (YYYY-MM-DD)
|
|
393
|
+
limit: Máximo de registros
|
|
394
|
+
"""
|
|
395
|
+
if purchase_order_id:
|
|
396
|
+
result = await make_sienge_request("GET", f"/purchase-orders/{purchase_order_id}")
|
|
397
|
+
if result["success"]:
|
|
398
|
+
return {
|
|
399
|
+
"success": True,
|
|
400
|
+
"message": f"✅ Pedido {purchase_order_id} encontrado",
|
|
401
|
+
"purchase_order": result["data"]
|
|
402
|
+
}
|
|
403
|
+
return result
|
|
404
|
+
|
|
405
|
+
params = {"limit": min(limit or 50, 200)}
|
|
406
|
+
if status:
|
|
407
|
+
params["status"] = status
|
|
408
|
+
if date_from:
|
|
409
|
+
params["date_from"] = date_from
|
|
410
|
+
|
|
411
|
+
result = await make_sienge_request("GET", "/purchase-orders", params=params)
|
|
412
|
+
|
|
413
|
+
if result["success"]:
|
|
414
|
+
data = result["data"]
|
|
415
|
+
orders = data.get("results", []) if isinstance(data, dict) else data
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
"success": True,
|
|
419
|
+
"message": f"✅ Encontrados {len(orders)} pedidos de compra",
|
|
420
|
+
"purchase_orders": orders,
|
|
421
|
+
"count": len(orders)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
"success": False,
|
|
426
|
+
"message": "❌ Erro ao buscar pedidos de compra",
|
|
427
|
+
"error": result.get("error"),
|
|
428
|
+
"details": result.get("message")
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
@mcp.tool
|
|
432
|
+
async def get_sienge_purchase_order_items(purchase_order_id: str) -> Dict:
|
|
433
|
+
"""
|
|
434
|
+
Consulta itens de um pedido de compra específico
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
purchase_order_id: ID do pedido (obrigatório)
|
|
438
|
+
"""
|
|
439
|
+
result = await make_sienge_request("GET", f"/purchase-orders/{purchase_order_id}/items")
|
|
440
|
+
|
|
441
|
+
if result["success"]:
|
|
442
|
+
data = result["data"]
|
|
443
|
+
items = data.get("results", []) if isinstance(data, dict) else data
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
"success": True,
|
|
447
|
+
"message": f"✅ Encontrados {len(items)} itens no pedido {purchase_order_id}",
|
|
448
|
+
"purchase_order_id": purchase_order_id,
|
|
449
|
+
"items": items,
|
|
450
|
+
"count": len(items)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
"success": False,
|
|
455
|
+
"message": f"❌ Erro ao buscar itens do pedido {purchase_order_id}",
|
|
456
|
+
"error": result.get("error"),
|
|
457
|
+
"details": result.get("message")
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
@mcp.tool
|
|
461
|
+
async def get_sienge_purchase_requests(purchase_request_id: Optional[str] = None, limit: Optional[int] = 50) -> Dict:
|
|
462
|
+
"""
|
|
463
|
+
Consulta solicitações de compra
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
purchase_request_id: ID específico da solicitação
|
|
467
|
+
limit: Máximo de registros
|
|
468
|
+
"""
|
|
469
|
+
if purchase_request_id:
|
|
470
|
+
result = await make_sienge_request("GET", f"/purchase-requests/{purchase_request_id}")
|
|
471
|
+
if result["success"]:
|
|
472
|
+
return {
|
|
473
|
+
"success": True,
|
|
474
|
+
"message": f"✅ Solicitação {purchase_request_id} encontrada",
|
|
475
|
+
"purchase_request": result["data"]
|
|
476
|
+
}
|
|
477
|
+
return result
|
|
478
|
+
|
|
479
|
+
params = {"limit": min(limit or 50, 200)}
|
|
480
|
+
result = await make_sienge_request("GET", "/purchase-requests", params=params)
|
|
481
|
+
|
|
482
|
+
if result["success"]:
|
|
483
|
+
data = result["data"]
|
|
484
|
+
requests = data.get("results", []) if isinstance(data, dict) else data
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
"success": True,
|
|
488
|
+
"message": f"✅ Encontradas {len(requests)} solicitações de compra",
|
|
489
|
+
"purchase_requests": requests,
|
|
490
|
+
"count": len(requests)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return {
|
|
494
|
+
"success": False,
|
|
495
|
+
"message": "❌ Erro ao buscar solicitações de compra",
|
|
496
|
+
"error": result.get("error"),
|
|
497
|
+
"details": result.get("message")
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
@mcp.tool
|
|
501
|
+
async def create_sienge_purchase_request(description: str, project_id: str, items: List[Dict[str, Any]]) -> Dict:
|
|
502
|
+
"""
|
|
503
|
+
Cria nova solicitação de compra
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
description: Descrição da solicitação
|
|
507
|
+
project_id: ID do projeto/obra
|
|
508
|
+
items: Lista de itens da solicitação
|
|
509
|
+
"""
|
|
510
|
+
request_data = {
|
|
511
|
+
"description": description,
|
|
512
|
+
"project_id": project_id,
|
|
513
|
+
"items": items,
|
|
514
|
+
"date": datetime.now().strftime("%Y-%m-%d")
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
result = await make_sienge_request("POST", "/purchase-requests", json_data=request_data)
|
|
518
|
+
|
|
519
|
+
if result["success"]:
|
|
520
|
+
return {
|
|
521
|
+
"success": True,
|
|
522
|
+
"message": "✅ Solicitação de compra criada com sucesso",
|
|
523
|
+
"request_id": result["data"].get("id"),
|
|
524
|
+
"data": result["data"]
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return {
|
|
528
|
+
"success": False,
|
|
529
|
+
"message": "❌ Erro ao criar solicitação de compra",
|
|
530
|
+
"error": result.get("error"),
|
|
531
|
+
"details": result.get("message")
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
# ============ ESTOQUE ============
|
|
535
|
+
|
|
536
|
+
@mcp.tool
|
|
537
|
+
async def get_sienge_stock_inventory(cost_center_id: str, resource_id: Optional[str] = None) -> Dict:
|
|
538
|
+
"""
|
|
539
|
+
Consulta inventário de estoque por centro de custo
|
|
540
|
+
|
|
541
|
+
Args:
|
|
542
|
+
cost_center_id: ID do centro de custo (obrigatório)
|
|
543
|
+
resource_id: ID do insumo específico (opcional)
|
|
544
|
+
"""
|
|
545
|
+
if resource_id:
|
|
546
|
+
endpoint = f"/stock-inventories/{cost_center_id}/items/{resource_id}"
|
|
547
|
+
else:
|
|
548
|
+
endpoint = f"/stock-inventories/{cost_center_id}/items"
|
|
549
|
+
|
|
550
|
+
result = await make_sienge_request("GET", endpoint)
|
|
551
|
+
|
|
552
|
+
if result["success"]:
|
|
553
|
+
data = result["data"]
|
|
554
|
+
items = data.get("results", []) if isinstance(data, dict) else data
|
|
555
|
+
count = len(items) if isinstance(items, list) else 1
|
|
556
|
+
|
|
557
|
+
return {
|
|
558
|
+
"success": True,
|
|
559
|
+
"message": f"✅ Inventário do centro de custo {cost_center_id}",
|
|
560
|
+
"cost_center_id": cost_center_id,
|
|
561
|
+
"inventory": items,
|
|
562
|
+
"count": count
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return {
|
|
566
|
+
"success": False,
|
|
567
|
+
"message": f"❌ Erro ao consultar estoque do centro {cost_center_id}",
|
|
568
|
+
"error": result.get("error"),
|
|
569
|
+
"details": result.get("message")
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
@mcp.tool
|
|
573
|
+
async def get_sienge_stock_reservations(limit: Optional[int] = 50) -> Dict:
|
|
574
|
+
"""
|
|
575
|
+
Lista reservas de estoque
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
limit: Máximo de registros
|
|
579
|
+
"""
|
|
580
|
+
params = {"limit": min(limit or 50, 200)}
|
|
581
|
+
result = await make_sienge_request("GET", "/stock-reservations", params=params)
|
|
582
|
+
|
|
583
|
+
if result["success"]:
|
|
584
|
+
data = result["data"]
|
|
585
|
+
reservations = data.get("results", []) if isinstance(data, dict) else data
|
|
586
|
+
|
|
587
|
+
return {
|
|
588
|
+
"success": True,
|
|
589
|
+
"message": f"✅ Encontradas {len(reservations)} reservas de estoque",
|
|
590
|
+
"reservations": reservations,
|
|
591
|
+
"count": len(reservations)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return {
|
|
595
|
+
"success": False,
|
|
596
|
+
"message": "❌ Erro ao buscar reservas de estoque",
|
|
597
|
+
"error": result.get("error"),
|
|
598
|
+
"details": result.get("message")
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
# ============ PROJETOS/OBRAS ============
|
|
602
|
+
|
|
603
|
+
@mcp.tool
|
|
604
|
+
async def get_sienge_projects(limit: Optional[int] = 50, offset: Optional[int] = 0, status: Optional[str] = None, search: Optional[str] = None) -> Dict:
|
|
605
|
+
"""
|
|
606
|
+
Busca projetos/obras no Sienge
|
|
607
|
+
|
|
608
|
+
Args:
|
|
609
|
+
limit: Máximo de registros (padrão: 50)
|
|
610
|
+
offset: Pular registros (padrão: 0)
|
|
611
|
+
status: Filtrar por status
|
|
612
|
+
search: Buscar por nome
|
|
613
|
+
"""
|
|
614
|
+
params = {"limit": min(limit or 50, 200), "offset": offset or 0}
|
|
615
|
+
|
|
616
|
+
if status:
|
|
617
|
+
params["status"] = status
|
|
618
|
+
if search:
|
|
619
|
+
params["search"] = search
|
|
620
|
+
|
|
621
|
+
result = await make_sienge_request("GET", "/projects", params=params)
|
|
622
|
+
|
|
623
|
+
if result["success"]:
|
|
624
|
+
data = result["data"]
|
|
625
|
+
projects = data.get("results", []) if isinstance(data, dict) else data
|
|
626
|
+
|
|
627
|
+
return {
|
|
628
|
+
"success": True,
|
|
629
|
+
"message": f"✅ Encontrados {len(projects)} projetos",
|
|
630
|
+
"projects": projects,
|
|
631
|
+
"count": len(projects)
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return {
|
|
635
|
+
"success": False,
|
|
636
|
+
"message": "❌ Erro ao buscar projetos",
|
|
637
|
+
"error": result.get("error"),
|
|
638
|
+
"details": result.get("message")
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
@mcp.tool
|
|
642
|
+
async def get_sienge_units(limit: Optional[int] = 50, offset: Optional[int] = 0) -> Dict:
|
|
643
|
+
"""
|
|
644
|
+
Consulta unidades cadastradas no Sienge
|
|
645
|
+
|
|
646
|
+
Args:
|
|
647
|
+
limit: Máximo de registros (padrão: 50)
|
|
648
|
+
offset: Pular registros (padrão: 0)
|
|
649
|
+
"""
|
|
650
|
+
params = {"limit": min(limit or 50, 200), "offset": offset or 0}
|
|
651
|
+
result = await make_sienge_request("GET", "/units", params=params)
|
|
652
|
+
|
|
653
|
+
if result["success"]:
|
|
654
|
+
data = result["data"]
|
|
655
|
+
units = data.get("results", []) if isinstance(data, dict) else data
|
|
656
|
+
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
657
|
+
total_count = metadata.get("count", len(units))
|
|
658
|
+
|
|
659
|
+
return {
|
|
660
|
+
"success": True,
|
|
661
|
+
"message": f"✅ Encontradas {len(units)} unidades (total: {total_count})",
|
|
662
|
+
"units": units,
|
|
663
|
+
"count": len(units)
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return {
|
|
667
|
+
"success": False,
|
|
668
|
+
"message": "❌ Erro ao buscar unidades",
|
|
669
|
+
"error": result.get("error"),
|
|
670
|
+
"details": result.get("message")
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
# ============ CUSTOS ============
|
|
674
|
+
|
|
675
|
+
@mcp.tool
|
|
676
|
+
async def get_sienge_unit_cost_tables(table_code: Optional[str] = None, description: Optional[str] = None,
|
|
677
|
+
status: Optional[str] = "Active", integration_enabled: Optional[bool] = None) -> Dict:
|
|
678
|
+
"""
|
|
679
|
+
Consulta tabelas de custos unitários
|
|
680
|
+
|
|
681
|
+
Args:
|
|
682
|
+
table_code: Código da tabela (opcional)
|
|
683
|
+
description: Descrição da tabela (opcional)
|
|
684
|
+
status: Status (Active/Inactive)
|
|
685
|
+
integration_enabled: Se habilitada para integração
|
|
686
|
+
"""
|
|
687
|
+
params = {"status": status or "Active"}
|
|
688
|
+
|
|
689
|
+
if table_code:
|
|
690
|
+
params["table_code"] = table_code
|
|
691
|
+
if description:
|
|
692
|
+
params["description"] = description
|
|
693
|
+
if integration_enabled is not None:
|
|
694
|
+
params["integration_enabled"] = integration_enabled
|
|
695
|
+
|
|
696
|
+
result = await make_sienge_request("GET", "/unit-cost-tables", params=params)
|
|
697
|
+
|
|
698
|
+
if result["success"]:
|
|
699
|
+
data = result["data"]
|
|
700
|
+
tables = data.get("results", []) if isinstance(data, dict) else data
|
|
701
|
+
|
|
702
|
+
return {
|
|
703
|
+
"success": True,
|
|
704
|
+
"message": f"✅ Encontradas {len(tables)} tabelas de custos",
|
|
705
|
+
"cost_tables": tables,
|
|
706
|
+
"count": len(tables)
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return {
|
|
710
|
+
"success": False,
|
|
711
|
+
"message": "❌ Erro ao buscar tabelas de custos",
|
|
712
|
+
"error": result.get("error"),
|
|
713
|
+
"details": result.get("message")
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
# ============ UTILITÁRIOS ============
|
|
717
|
+
|
|
718
|
+
@mcp.tool
|
|
719
|
+
def add(a: int, b: int) -> int:
|
|
720
|
+
"""Soma dois números (função de teste)"""
|
|
721
|
+
return a + b
|
|
722
|
+
|
|
723
|
+
def _get_auth_info_internal() -> Dict:
|
|
724
|
+
"""Função interna para verificar configuração de autenticação"""
|
|
725
|
+
if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
|
|
726
|
+
return {
|
|
727
|
+
"auth_method": "Bearer Token",
|
|
728
|
+
"configured": True,
|
|
729
|
+
"base_url": SIENGE_BASE_URL,
|
|
730
|
+
"api_key_configured": True
|
|
731
|
+
}
|
|
732
|
+
elif SIENGE_USERNAME and SIENGE_PASSWORD:
|
|
733
|
+
return {
|
|
734
|
+
"auth_method": "Basic Auth",
|
|
735
|
+
"configured": True,
|
|
736
|
+
"base_url": SIENGE_BASE_URL,
|
|
737
|
+
"subdomain": SIENGE_SUBDOMAIN,
|
|
738
|
+
"username": SIENGE_USERNAME
|
|
739
|
+
}
|
|
740
|
+
else:
|
|
741
|
+
return {
|
|
742
|
+
"auth_method": "None",
|
|
743
|
+
"configured": False,
|
|
744
|
+
"message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env"
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
@mcp.tool
|
|
748
|
+
def get_auth_info() -> Dict:
|
|
749
|
+
"""Retorna informações sobre a configuração de autenticação"""
|
|
750
|
+
return _get_auth_info_internal()
|
|
751
|
+
|
|
752
|
+
def main():
|
|
753
|
+
"""Entry point for the Sienge MCP Server"""
|
|
754
|
+
print("* Iniciando Sienge MCP Server (FastMCP)...")
|
|
755
|
+
|
|
756
|
+
# Mostrar info de configuração
|
|
757
|
+
auth_info = _get_auth_info_internal()
|
|
758
|
+
print(f"* Autenticacao: {auth_info['auth_method']}")
|
|
759
|
+
print(f"* Configurado: {auth_info['configured']}")
|
|
760
|
+
|
|
761
|
+
if not auth_info['configured']:
|
|
762
|
+
print("* ERRO: Autenticacao nao configurada!")
|
|
763
|
+
print("Configure as variáveis de ambiente:")
|
|
764
|
+
print("- SIENGE_API_KEY (Bearer Token) OU")
|
|
765
|
+
print("- SIENGE_USERNAME + SIENGE_PASSWORD + SIENGE_SUBDOMAIN (Basic Auth)")
|
|
766
|
+
print("- SIENGE_BASE_URL (padrão: https://api.sienge.com.br)")
|
|
767
|
+
print("")
|
|
768
|
+
print("Exemplo no Windows PowerShell:")
|
|
769
|
+
print('$env:SIENGE_USERNAME="seu_usuario"')
|
|
770
|
+
print('$env:SIENGE_PASSWORD="sua_senha"')
|
|
771
|
+
print('$env:SIENGE_SUBDOMAIN="sua_empresa"')
|
|
772
|
+
print("")
|
|
773
|
+
print("Exemplo no Linux/Mac:")
|
|
774
|
+
print('export SIENGE_USERNAME="seu_usuario"')
|
|
775
|
+
print('export SIENGE_PASSWORD="sua_senha"')
|
|
776
|
+
print('export SIENGE_SUBDOMAIN="sua_empresa"')
|
|
777
|
+
else:
|
|
778
|
+
print("* MCP pronto para uso!")
|
|
779
|
+
|
|
780
|
+
mcp.run()
|
|
781
|
+
|
|
782
|
+
if __name__ == "__main__":
|
|
783
|
+
main()
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SPDX-FileCopyrightText: © 2025 Moizes Silva
|
|
3
|
+
SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
Logger module for Sienge MCP Server
|
|
6
|
+
|
|
7
|
+
This module provides logging functionality for the server,
|
|
8
|
+
writing logs to a file to avoid interfering with MCP communication.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Optional, Dict
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LogLevel(Enum):
|
|
21
|
+
"""Log levels enum"""
|
|
22
|
+
TRACE = 5
|
|
23
|
+
DEBUG = 10
|
|
24
|
+
INFO = 20
|
|
25
|
+
WARN = 30
|
|
26
|
+
ERROR = 40
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SiengeLogger:
|
|
30
|
+
"""
|
|
31
|
+
Professional logging system for Sienge MCP Server
|
|
32
|
+
Based on ClickUp MCP logging patterns
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, name: str = "SiengeMCP"):
|
|
36
|
+
self.name = name
|
|
37
|
+
self.pid = os.getpid()
|
|
38
|
+
self.logger = self._setup_logger()
|
|
39
|
+
|
|
40
|
+
def _setup_logger(self) -> logging.Logger:
|
|
41
|
+
"""Setup logger with file output only"""
|
|
42
|
+
logger = logging.getLogger(self.name)
|
|
43
|
+
logger.setLevel(logging.DEBUG)
|
|
44
|
+
|
|
45
|
+
# Prevent duplicate handlers
|
|
46
|
+
if logger.handlers:
|
|
47
|
+
return logger
|
|
48
|
+
|
|
49
|
+
# Create logs directory if it doesn't exist
|
|
50
|
+
log_dir = Path(__file__).parent.parent.parent.parent / "logs"
|
|
51
|
+
log_dir.mkdir(exist_ok=True)
|
|
52
|
+
|
|
53
|
+
# Create file handler
|
|
54
|
+
log_file = log_dir / "sienge-mcp.log"
|
|
55
|
+
file_handler = logging.FileHandler(log_file, mode='w', encoding='utf-8')
|
|
56
|
+
|
|
57
|
+
# Create formatter
|
|
58
|
+
formatter = logging.Formatter(
|
|
59
|
+
'[%(asctime)s] [PID:%(process)d] %(levelname)s [%(name)s]: %(message)s',
|
|
60
|
+
datefmt='%Y-%m-%dT%H:%M:%S.%fZ'
|
|
61
|
+
)
|
|
62
|
+
file_handler.setFormatter(formatter)
|
|
63
|
+
|
|
64
|
+
logger.addHandler(file_handler)
|
|
65
|
+
|
|
66
|
+
# Log initialization
|
|
67
|
+
logger.info(f"Logger initialized for {self.name}")
|
|
68
|
+
|
|
69
|
+
return logger
|
|
70
|
+
|
|
71
|
+
def _format_data(self, data: Any) -> str:
|
|
72
|
+
"""Format data for logging"""
|
|
73
|
+
if data is None:
|
|
74
|
+
return ""
|
|
75
|
+
|
|
76
|
+
if isinstance(data, dict):
|
|
77
|
+
if len(data) <= 4 and all(not isinstance(v, (dict, list)) or v is None for v in data.values()):
|
|
78
|
+
# Simple object with few properties - format inline
|
|
79
|
+
items = [f"{k}={v}" for k, v in data.items()]
|
|
80
|
+
return f" | {' | '.join(items)}"
|
|
81
|
+
else:
|
|
82
|
+
# Complex object - format as JSON
|
|
83
|
+
import json
|
|
84
|
+
try:
|
|
85
|
+
return f" | {json.dumps(data, indent=2, ensure_ascii=False)}"
|
|
86
|
+
except (TypeError, ValueError):
|
|
87
|
+
return f" | {str(data)}"
|
|
88
|
+
|
|
89
|
+
return f" | {str(data)}"
|
|
90
|
+
|
|
91
|
+
def trace(self, message: str, data: Optional[Dict[str, Any]] = None):
|
|
92
|
+
"""Log trace level message"""
|
|
93
|
+
log_message = message + (self._format_data(data) if data else "")
|
|
94
|
+
self.logger.log(LogLevel.TRACE.value, log_message)
|
|
95
|
+
|
|
96
|
+
def debug(self, message: str, data: Optional[Dict[str, Any]] = None):
|
|
97
|
+
"""Log debug level message"""
|
|
98
|
+
log_message = message + (self._format_data(data) if data else "")
|
|
99
|
+
self.logger.debug(log_message)
|
|
100
|
+
|
|
101
|
+
def info(self, message: str, data: Optional[Dict[str, Any]] = None):
|
|
102
|
+
"""Log info level message"""
|
|
103
|
+
log_message = message + (self._format_data(data) if data else "")
|
|
104
|
+
self.logger.info(log_message)
|
|
105
|
+
|
|
106
|
+
def warn(self, message: str, data: Optional[Dict[str, Any]] = None):
|
|
107
|
+
"""Log warning level message"""
|
|
108
|
+
log_message = message + (self._format_data(data) if data else "")
|
|
109
|
+
self.logger.warning(log_message)
|
|
110
|
+
|
|
111
|
+
def error(self, message: str, data: Optional[Dict[str, Any]] = None):
|
|
112
|
+
"""Log error level message"""
|
|
113
|
+
log_message = message + (self._format_data(data) if data else "")
|
|
114
|
+
self.logger.error(log_message)
|
|
115
|
+
|
|
116
|
+
def log_operation(self, operation: str, data: Optional[Dict[str, Any]] = None):
|
|
117
|
+
"""Log API operation with data"""
|
|
118
|
+
self.debug(f"Operation: {operation}", data)
|
|
119
|
+
|
|
120
|
+
def log_request(self, method: str, url: str, data: Optional[Dict[str, Any]] = None):
|
|
121
|
+
"""Log HTTP request"""
|
|
122
|
+
request_data = {"method": method, "url": url}
|
|
123
|
+
if data:
|
|
124
|
+
request_data["data"] = data
|
|
125
|
+
self.debug("HTTP Request", request_data)
|
|
126
|
+
|
|
127
|
+
def log_response(self, status_code: int, url: str, data: Optional[Dict[str, Any]] = None):
|
|
128
|
+
"""Log HTTP response"""
|
|
129
|
+
response_data = {"status": status_code, "url": url}
|
|
130
|
+
if data:
|
|
131
|
+
response_data["response"] = data
|
|
132
|
+
self.debug("HTTP Response", response_data)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# Global logger instances
|
|
136
|
+
_loggers: Dict[str, SiengeLogger] = {}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def get_logger(name: str = "SiengeMCP") -> SiengeLogger:
|
|
140
|
+
"""Get or create a logger instance"""
|
|
141
|
+
if name not in _loggers:
|
|
142
|
+
_loggers[name] = SiengeLogger(name)
|
|
143
|
+
return _loggers[name]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# Convenience functions for default logger
|
|
147
|
+
def trace(message: str, data: Optional[Dict[str, Any]] = None):
|
|
148
|
+
"""Log trace level message using default logger"""
|
|
149
|
+
get_logger().trace(message, data)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def debug(message: str, data: Optional[Dict[str, Any]] = None):
|
|
153
|
+
"""Log debug level message using default logger"""
|
|
154
|
+
get_logger().debug(message, data)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def info(message: str, data: Optional[Dict[str, Any]] = None):
|
|
158
|
+
"""Log info level message using default logger"""
|
|
159
|
+
get_logger().info(message, data)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def warn(message: str, data: Optional[Dict[str, Any]] = None):
|
|
163
|
+
"""Log warning level message using default logger"""
|
|
164
|
+
get_logger().warn(message, data)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def error(message: str, data: Optional[Dict[str, Any]] = None):
|
|
168
|
+
"""Log error level message using default logger"""
|
|
169
|
+
get_logger().error(message, data)
|