sienge-ecbiesek-mcp 1.2.3__tar.gz → 1.3.0__tar.gz
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.2.3 → sienge_ecbiesek_mcp-1.3.0}/PKG-INFO +34 -3
- {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/README.md +31 -1
- {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/pyproject.toml +3 -2
- {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/requirements.txt +3 -0
- {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/src/sienge_ecbiesek_mcp.egg-info/PKG-INFO +34 -3
- {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/src/sienge_ecbiesek_mcp.egg-info/requires.txt +1 -0
- {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/src/sienge_mcp/server.py +748 -97
- {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/src/sienge_mcp/utils/logger.py +1 -1
- {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/COMO_TESTAR.md +0 -0
- {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/GUIA_ATUALIZACAO.md +0 -0
- {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/LICENSE +0 -0
- {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/MANIFEST.in +0 -0
- {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/PUBLICADO_PYPI.md +0 -0
- {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/claude_desktop_config.template.json +0 -0
- {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/setup.cfg +0 -0
- {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/src/sienge_ecbiesek_mcp.egg-info/SOURCES.txt +0 -0
- {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/src/sienge_ecbiesek_mcp.egg-info/dependency_links.txt +0 -0
- {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/src/sienge_ecbiesek_mcp.egg-info/entry_points.txt +0 -0
- {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/src/sienge_ecbiesek_mcp.egg-info/top_level.txt +0 -0
- {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/src/sienge_mcp/__init__.py +0 -0
- {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/src/sienge_mcp/metadata.py +0 -0
- {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/src/sienge_mcp/server2.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sienge-ecbiesek-mcp
|
|
3
|
-
Version: 1.
|
|
4
|
-
Summary: 🏗️ Model Context Protocol (MCP) server for Sienge API integration - Brazilian construction ERP system. Connect Claude AI to Sienge with 50+ powerful tools for comprehensive business management including financials, projects, and
|
|
3
|
+
Version: 1.3.0
|
|
4
|
+
Summary: 🏗️ Model Context Protocol (MCP) server for Sienge API integration - Brazilian construction ERP system. Connect Claude AI to Sienge with 50+ powerful tools for comprehensive business management including financials, projects, operations, and Supabase database queries.
|
|
5
5
|
Author-email: ECBIESEK <ti@ecbiesek.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/INOTECH-ecbiesek/Sienge-MCP
|
|
@@ -33,6 +33,7 @@ Requires-Dist: fastmcp>=2.12.3
|
|
|
33
33
|
Requires-Dist: httpx>=0.25.0
|
|
34
34
|
Requires-Dist: pydantic>=2.0.0
|
|
35
35
|
Requires-Dist: python-dotenv>=1.0.0
|
|
36
|
+
Requires-Dist: supabase>=2.0.0
|
|
36
37
|
Provides-Extra: dev
|
|
37
38
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
38
39
|
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
@@ -67,6 +68,19 @@ Um servidor Model Context Protocol (MCP) para integração com a API do Sienge,
|
|
|
67
68
|
### 🔍 Solicitações de Compra
|
|
68
69
|
- **get_sienge_purchase_requests**: Lista solicitações de compra do sistema
|
|
69
70
|
|
|
71
|
+
### 🗄️ Consultas Supabase
|
|
72
|
+
- **query_supabase_database**: Executa queries no banco de dados Supabase
|
|
73
|
+
- **get_supabase_table_info**: Obtém informações sobre tabelas disponíveis
|
|
74
|
+
- **search_supabase_data**: Busca universal em múltiplas tabelas
|
|
75
|
+
- Suporte a filtros, ordenação e busca textual/inteligente
|
|
76
|
+
- Schema fixo `sienge_data` para organização dos dados
|
|
77
|
+
|
|
78
|
+
### 🔍 Busca Universal
|
|
79
|
+
- **search_sienge_data**: Busca unificada em múltiplas entidades do Sienge
|
|
80
|
+
- **search_sienge_financial_data**: Busca avançada em dados financeiros
|
|
81
|
+
- **get_sienge_data_paginated**: Paginação avançada para grandes volumes
|
|
82
|
+
- **get_sienge_dashboard_summary**: Resumo executivo do sistema
|
|
83
|
+
|
|
70
84
|
## 📦 Instalação
|
|
71
85
|
|
|
72
86
|
### Via PyPI (Recomendado)
|
|
@@ -93,6 +107,10 @@ SIENGE_SUBDOMAIN=seu_subdominio
|
|
|
93
107
|
SIENGE_USERNAME=seu_usuario
|
|
94
108
|
SIENGE_PASSWORD=sua_senha
|
|
95
109
|
SIENGE_TIMEOUT=30
|
|
110
|
+
|
|
111
|
+
# Configurações do Supabase (opcional)
|
|
112
|
+
SUPABASE_URL=https://seu-projeto.supabase.co
|
|
113
|
+
SUPABASE_SERVICE_ROLE_KEY=sua_service_role_key
|
|
96
114
|
```
|
|
97
115
|
|
|
98
116
|
### 2. Configuração no Claude Desktop
|
|
@@ -244,6 +262,7 @@ pytest tests/
|
|
|
244
262
|
- httpx >= 0.25.0
|
|
245
263
|
- pydantic >= 2.0.0
|
|
246
264
|
- python-dotenv >= 1.0.0
|
|
265
|
+
- supabase >= 2.0.0
|
|
247
266
|
|
|
248
267
|
### Compatibilidade
|
|
249
268
|
- ✅ Windows
|
|
@@ -368,7 +387,19 @@ Este projeto está licenciado sob a licença MIT. Veja o arquivo [LICENSE](LICEN
|
|
|
368
387
|
|
|
369
388
|
## 📈 Versões
|
|
370
389
|
|
|
371
|
-
### v1.
|
|
390
|
+
### v1.3.0 (Atual)
|
|
391
|
+
- ✅ **NOVO**: Integração completa com Supabase
|
|
392
|
+
- ✅ **NOVO**: 3 ferramentas de consulta ao banco de dados
|
|
393
|
+
- ✅ **NOVO**: Busca universal em múltiplas tabelas
|
|
394
|
+
- ✅ **NOVO**: Busca inteligente (textual + numérica)
|
|
395
|
+
- ✅ **NOVO**: Dashboard executivo do sistema
|
|
396
|
+
- ✅ **NOVO**: Paginação avançada para grandes volumes
|
|
397
|
+
- ✅ **NOVO**: Busca financeira unificada
|
|
398
|
+
- ✅ **MELHORADO**: Validação de parâmetros robusta
|
|
399
|
+
- ✅ **MELHORADO**: Tratamento de erros aprimorado
|
|
400
|
+
- ✅ **MELHORADO**: Documentação completa atualizada
|
|
401
|
+
|
|
402
|
+
### v1.2.3
|
|
372
403
|
- ✅ Adicionadas 5 ferramentas para Notas Fiscais de Compra
|
|
373
404
|
- ✅ Suporte à Bulk-data API para contas a receber
|
|
374
405
|
- ✅ Correção de endpoints para projetos/empresas
|
|
@@ -23,6 +23,19 @@ Um servidor Model Context Protocol (MCP) para integração com a API do Sienge,
|
|
|
23
23
|
### 🔍 Solicitações de Compra
|
|
24
24
|
- **get_sienge_purchase_requests**: Lista solicitações de compra do sistema
|
|
25
25
|
|
|
26
|
+
### 🗄️ Consultas Supabase
|
|
27
|
+
- **query_supabase_database**: Executa queries no banco de dados Supabase
|
|
28
|
+
- **get_supabase_table_info**: Obtém informações sobre tabelas disponíveis
|
|
29
|
+
- **search_supabase_data**: Busca universal em múltiplas tabelas
|
|
30
|
+
- Suporte a filtros, ordenação e busca textual/inteligente
|
|
31
|
+
- Schema fixo `sienge_data` para organização dos dados
|
|
32
|
+
|
|
33
|
+
### 🔍 Busca Universal
|
|
34
|
+
- **search_sienge_data**: Busca unificada em múltiplas entidades do Sienge
|
|
35
|
+
- **search_sienge_financial_data**: Busca avançada em dados financeiros
|
|
36
|
+
- **get_sienge_data_paginated**: Paginação avançada para grandes volumes
|
|
37
|
+
- **get_sienge_dashboard_summary**: Resumo executivo do sistema
|
|
38
|
+
|
|
26
39
|
## 📦 Instalação
|
|
27
40
|
|
|
28
41
|
### Via PyPI (Recomendado)
|
|
@@ -49,6 +62,10 @@ SIENGE_SUBDOMAIN=seu_subdominio
|
|
|
49
62
|
SIENGE_USERNAME=seu_usuario
|
|
50
63
|
SIENGE_PASSWORD=sua_senha
|
|
51
64
|
SIENGE_TIMEOUT=30
|
|
65
|
+
|
|
66
|
+
# Configurações do Supabase (opcional)
|
|
67
|
+
SUPABASE_URL=https://seu-projeto.supabase.co
|
|
68
|
+
SUPABASE_SERVICE_ROLE_KEY=sua_service_role_key
|
|
52
69
|
```
|
|
53
70
|
|
|
54
71
|
### 2. Configuração no Claude Desktop
|
|
@@ -200,6 +217,7 @@ pytest tests/
|
|
|
200
217
|
- httpx >= 0.25.0
|
|
201
218
|
- pydantic >= 2.0.0
|
|
202
219
|
- python-dotenv >= 1.0.0
|
|
220
|
+
- supabase >= 2.0.0
|
|
203
221
|
|
|
204
222
|
### Compatibilidade
|
|
205
223
|
- ✅ Windows
|
|
@@ -324,7 +342,19 @@ Este projeto está licenciado sob a licença MIT. Veja o arquivo [LICENSE](LICEN
|
|
|
324
342
|
|
|
325
343
|
## 📈 Versões
|
|
326
344
|
|
|
327
|
-
### v1.
|
|
345
|
+
### v1.3.0 (Atual)
|
|
346
|
+
- ✅ **NOVO**: Integração completa com Supabase
|
|
347
|
+
- ✅ **NOVO**: 3 ferramentas de consulta ao banco de dados
|
|
348
|
+
- ✅ **NOVO**: Busca universal em múltiplas tabelas
|
|
349
|
+
- ✅ **NOVO**: Busca inteligente (textual + numérica)
|
|
350
|
+
- ✅ **NOVO**: Dashboard executivo do sistema
|
|
351
|
+
- ✅ **NOVO**: Paginação avançada para grandes volumes
|
|
352
|
+
- ✅ **NOVO**: Busca financeira unificada
|
|
353
|
+
- ✅ **MELHORADO**: Validação de parâmetros robusta
|
|
354
|
+
- ✅ **MELHORADO**: Tratamento de erros aprimorado
|
|
355
|
+
- ✅ **MELHORADO**: Documentação completa atualizada
|
|
356
|
+
|
|
357
|
+
### v1.2.3
|
|
328
358
|
- ✅ Adicionadas 5 ferramentas para Notas Fiscais de Compra
|
|
329
359
|
- ✅ Suporte à Bulk-data API para contas a receber
|
|
330
360
|
- ✅ Correção de endpoints para projetos/empresas
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sienge-ecbiesek-mcp"
|
|
7
|
-
version = "1.
|
|
8
|
-
description = "🏗️ Model Context Protocol (MCP) server for Sienge API integration - Brazilian construction ERP system. Connect Claude AI to Sienge with 50+ powerful tools for comprehensive business management including financials, projects, and
|
|
7
|
+
version = "1.3.0"
|
|
8
|
+
description = "🏗️ Model Context Protocol (MCP) server for Sienge API integration - Brazilian construction ERP system. Connect Claude AI to Sienge with 50+ powerful tools for comprehensive business management including financials, projects, operations, and Supabase database queries."
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "ECBIESEK", email = "ti@ecbiesek.com"}
|
|
11
11
|
]
|
|
@@ -47,6 +47,7 @@ dependencies = [
|
|
|
47
47
|
"httpx>=0.25.0",
|
|
48
48
|
"pydantic>=2.0.0",
|
|
49
49
|
"python-dotenv>=1.0.0",
|
|
50
|
+
"supabase>=2.0.0",
|
|
50
51
|
]
|
|
51
52
|
|
|
52
53
|
[project.optional-dependencies]
|
{sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/src/sienge_ecbiesek_mcp.egg-info/PKG-INFO
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sienge-ecbiesek-mcp
|
|
3
|
-
Version: 1.
|
|
4
|
-
Summary: 🏗️ Model Context Protocol (MCP) server for Sienge API integration - Brazilian construction ERP system. Connect Claude AI to Sienge with 50+ powerful tools for comprehensive business management including financials, projects, and
|
|
3
|
+
Version: 1.3.0
|
|
4
|
+
Summary: 🏗️ Model Context Protocol (MCP) server for Sienge API integration - Brazilian construction ERP system. Connect Claude AI to Sienge with 50+ powerful tools for comprehensive business management including financials, projects, operations, and Supabase database queries.
|
|
5
5
|
Author-email: ECBIESEK <ti@ecbiesek.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/INOTECH-ecbiesek/Sienge-MCP
|
|
@@ -33,6 +33,7 @@ Requires-Dist: fastmcp>=2.12.3
|
|
|
33
33
|
Requires-Dist: httpx>=0.25.0
|
|
34
34
|
Requires-Dist: pydantic>=2.0.0
|
|
35
35
|
Requires-Dist: python-dotenv>=1.0.0
|
|
36
|
+
Requires-Dist: supabase>=2.0.0
|
|
36
37
|
Provides-Extra: dev
|
|
37
38
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
38
39
|
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
@@ -67,6 +68,19 @@ Um servidor Model Context Protocol (MCP) para integração com a API do Sienge,
|
|
|
67
68
|
### 🔍 Solicitações de Compra
|
|
68
69
|
- **get_sienge_purchase_requests**: Lista solicitações de compra do sistema
|
|
69
70
|
|
|
71
|
+
### 🗄️ Consultas Supabase
|
|
72
|
+
- **query_supabase_database**: Executa queries no banco de dados Supabase
|
|
73
|
+
- **get_supabase_table_info**: Obtém informações sobre tabelas disponíveis
|
|
74
|
+
- **search_supabase_data**: Busca universal em múltiplas tabelas
|
|
75
|
+
- Suporte a filtros, ordenação e busca textual/inteligente
|
|
76
|
+
- Schema fixo `sienge_data` para organização dos dados
|
|
77
|
+
|
|
78
|
+
### 🔍 Busca Universal
|
|
79
|
+
- **search_sienge_data**: Busca unificada em múltiplas entidades do Sienge
|
|
80
|
+
- **search_sienge_financial_data**: Busca avançada em dados financeiros
|
|
81
|
+
- **get_sienge_data_paginated**: Paginação avançada para grandes volumes
|
|
82
|
+
- **get_sienge_dashboard_summary**: Resumo executivo do sistema
|
|
83
|
+
|
|
70
84
|
## 📦 Instalação
|
|
71
85
|
|
|
72
86
|
### Via PyPI (Recomendado)
|
|
@@ -93,6 +107,10 @@ SIENGE_SUBDOMAIN=seu_subdominio
|
|
|
93
107
|
SIENGE_USERNAME=seu_usuario
|
|
94
108
|
SIENGE_PASSWORD=sua_senha
|
|
95
109
|
SIENGE_TIMEOUT=30
|
|
110
|
+
|
|
111
|
+
# Configurações do Supabase (opcional)
|
|
112
|
+
SUPABASE_URL=https://seu-projeto.supabase.co
|
|
113
|
+
SUPABASE_SERVICE_ROLE_KEY=sua_service_role_key
|
|
96
114
|
```
|
|
97
115
|
|
|
98
116
|
### 2. Configuração no Claude Desktop
|
|
@@ -244,6 +262,7 @@ pytest tests/
|
|
|
244
262
|
- httpx >= 0.25.0
|
|
245
263
|
- pydantic >= 2.0.0
|
|
246
264
|
- python-dotenv >= 1.0.0
|
|
265
|
+
- supabase >= 2.0.0
|
|
247
266
|
|
|
248
267
|
### Compatibilidade
|
|
249
268
|
- ✅ Windows
|
|
@@ -368,7 +387,19 @@ Este projeto está licenciado sob a licença MIT. Veja o arquivo [LICENSE](LICEN
|
|
|
368
387
|
|
|
369
388
|
## 📈 Versões
|
|
370
389
|
|
|
371
|
-
### v1.
|
|
390
|
+
### v1.3.0 (Atual)
|
|
391
|
+
- ✅ **NOVO**: Integração completa com Supabase
|
|
392
|
+
- ✅ **NOVO**: 3 ferramentas de consulta ao banco de dados
|
|
393
|
+
- ✅ **NOVO**: Busca universal em múltiplas tabelas
|
|
394
|
+
- ✅ **NOVO**: Busca inteligente (textual + numérica)
|
|
395
|
+
- ✅ **NOVO**: Dashboard executivo do sistema
|
|
396
|
+
- ✅ **NOVO**: Paginação avançada para grandes volumes
|
|
397
|
+
- ✅ **NOVO**: Busca financeira unificada
|
|
398
|
+
- ✅ **MELHORADO**: Validação de parâmetros robusta
|
|
399
|
+
- ✅ **MELHORADO**: Tratamento de erros aprimorado
|
|
400
|
+
- ✅ **MELHORADO**: Documentação completa atualizada
|
|
401
|
+
|
|
402
|
+
### v1.2.3
|
|
372
403
|
- ✅ Adicionadas 5 ferramentas para Notas Fiscais de Compra
|
|
373
404
|
- ✅ Suporte à Bulk-data API para contas a receber
|
|
374
405
|
- ✅ Correção de endpoints para projetos/empresas
|
|
@@ -12,6 +12,11 @@ from dotenv import load_dotenv
|
|
|
12
12
|
from datetime import datetime
|
|
13
13
|
import time
|
|
14
14
|
import uuid
|
|
15
|
+
import asyncio
|
|
16
|
+
|
|
17
|
+
# logger
|
|
18
|
+
from .utils.logger import get_logger
|
|
19
|
+
logger = get_logger()
|
|
15
20
|
|
|
16
21
|
# Optional: prefer tenacity for robust retries; linter will warn if not installed but code falls back
|
|
17
22
|
try:
|
|
@@ -20,6 +25,15 @@ try:
|
|
|
20
25
|
except Exception:
|
|
21
26
|
TENACITY_AVAILABLE = False
|
|
22
27
|
|
|
28
|
+
# Supabase client (optional)
|
|
29
|
+
try:
|
|
30
|
+
from supabase import create_client, Client
|
|
31
|
+
SUPABASE_AVAILABLE = True
|
|
32
|
+
except Exception:
|
|
33
|
+
SUPABASE_AVAILABLE = False
|
|
34
|
+
create_client = None
|
|
35
|
+
Client = None
|
|
36
|
+
|
|
23
37
|
# Carrega as variáveis de ambiente
|
|
24
38
|
load_dotenv()
|
|
25
39
|
|
|
@@ -33,6 +47,11 @@ SIENGE_PASSWORD = os.getenv("SIENGE_PASSWORD", "")
|
|
|
33
47
|
SIENGE_API_KEY = os.getenv("SIENGE_API_KEY", "")
|
|
34
48
|
REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", "30"))
|
|
35
49
|
|
|
50
|
+
# Configurações do Supabase
|
|
51
|
+
SUPABASE_URL = os.getenv("SUPABASE_URL")
|
|
52
|
+
SUPABASE_SERVICE_ROLE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY")
|
|
53
|
+
SUPABASE_SCHEMA = "sienge_data" # Schema fixo: sienge_data
|
|
54
|
+
|
|
36
55
|
|
|
37
56
|
class SiengeAPIError(Exception):
|
|
38
57
|
"""Exceção customizada para erros da API do Sienge"""
|
|
@@ -74,43 +93,51 @@ async def make_sienge_request(
|
|
|
74
93
|
return await client.request(method=method, url=url, headers=headers, params=params, json=json_data, auth=auth)
|
|
75
94
|
|
|
76
95
|
try:
|
|
96
|
+
max_attempts = 5
|
|
97
|
+
attempts = 0
|
|
77
98
|
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
async for attempt in AsyncRetrying(stop=stop_after_attempt(3), wait=wait_exponential(min=1, max=8), retry=retry_if_exception_type((httpx.RequestError, httpx.HTTPStatusError))):
|
|
81
|
-
with attempt:
|
|
82
|
-
response = await _do_request(client)
|
|
83
|
-
else:
|
|
84
|
-
# Simple manual retry with exponential backoff
|
|
85
|
-
attempts = 0
|
|
86
|
-
while True:
|
|
87
|
-
try:
|
|
88
|
-
response = await _do_request(client)
|
|
89
|
-
break
|
|
90
|
-
except (httpx.RequestError, httpx.TimeoutException) as exc:
|
|
91
|
-
attempts += 1
|
|
92
|
-
if attempts >= 3:
|
|
93
|
-
raise
|
|
94
|
-
await client.aclose()
|
|
95
|
-
await httpx.AsyncClient().aclose()
|
|
96
|
-
await __import__('asyncio').sleep(2 ** attempts)
|
|
97
|
-
|
|
98
|
-
latency_ms = int((time.time() - start_ts) * 1000)
|
|
99
|
-
|
|
100
|
-
if response.status_code in [200, 201]:
|
|
99
|
+
while True:
|
|
100
|
+
attempts += 1
|
|
101
101
|
try:
|
|
102
|
-
|
|
103
|
-
except
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
"
|
|
113
|
-
|
|
102
|
+
response = await client.request(method=method, url=url, headers=headers, params=params, json=json_data, auth=auth)
|
|
103
|
+
except (httpx.RequestError, httpx.TimeoutException) as exc:
|
|
104
|
+
logger.warning(f"Request error to {url}: {exc} (attempt {attempts}/{max_attempts})")
|
|
105
|
+
if attempts >= max_attempts:
|
|
106
|
+
raise
|
|
107
|
+
await asyncio.sleep(min(2 ** attempts, 60))
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
# Handle rate limit explicitly
|
|
111
|
+
if response.status_code == 429:
|
|
112
|
+
retry_after = response.headers.get("Retry-After")
|
|
113
|
+
try:
|
|
114
|
+
wait_seconds = int(retry_after) if retry_after is not None else min(2 ** attempts, 60)
|
|
115
|
+
except Exception:
|
|
116
|
+
wait_seconds = min(2 ** attempts, 60)
|
|
117
|
+
logger.warning(f"HTTP 429 from {url}, retrying after {wait_seconds}s (attempt {attempts}/{max_attempts})")
|
|
118
|
+
if attempts >= max_attempts:
|
|
119
|
+
latency_ms = int((time.time() - start_ts) * 1000)
|
|
120
|
+
return {"success": False, "error": "HTTP 429", "message": response.text, "status_code": 429, "latency_ms": latency_ms, "request_id": request_id}
|
|
121
|
+
await asyncio.sleep(wait_seconds)
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
latency_ms = int((time.time() - start_ts) * 1000)
|
|
125
|
+
|
|
126
|
+
if response.status_code in [200, 201]:
|
|
127
|
+
try:
|
|
128
|
+
return {"success": True, "data": response.json(), "status_code": response.status_code, "latency_ms": latency_ms, "request_id": request_id}
|
|
129
|
+
except BaseException:
|
|
130
|
+
return {"success": True, "data": {"message": "Success"}, "status_code": response.status_code, "latency_ms": latency_ms, "request_id": request_id}
|
|
131
|
+
else:
|
|
132
|
+
logger.warning(f"HTTP {response.status_code} from {url}: {response.text}")
|
|
133
|
+
return {
|
|
134
|
+
"success": False,
|
|
135
|
+
"error": f"HTTP {response.status_code}",
|
|
136
|
+
"message": response.text,
|
|
137
|
+
"status_code": response.status_code,
|
|
138
|
+
"latency_ms": latency_ms,
|
|
139
|
+
"request_id": request_id,
|
|
140
|
+
}
|
|
114
141
|
|
|
115
142
|
except httpx.TimeoutException:
|
|
116
143
|
latency_ms = int((time.time() - start_ts) * 1000)
|
|
@@ -152,39 +179,50 @@ async def make_sienge_bulk_request(
|
|
|
152
179
|
return await client.request(method=method, url=url, headers=headers, params=params, json=json_data, auth=auth)
|
|
153
180
|
|
|
154
181
|
try:
|
|
182
|
+
max_attempts = 5
|
|
183
|
+
attempts = 0
|
|
155
184
|
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
with attempt:
|
|
159
|
-
response = await _do_request(client)
|
|
160
|
-
else:
|
|
161
|
-
attempts = 0
|
|
162
|
-
while True:
|
|
163
|
-
try:
|
|
164
|
-
response = await _do_request(client)
|
|
165
|
-
break
|
|
166
|
-
except (httpx.RequestError, httpx.TimeoutException) as exc:
|
|
167
|
-
attempts += 1
|
|
168
|
-
if attempts >= 3:
|
|
169
|
-
raise
|
|
170
|
-
await __import__('asyncio').sleep(2 ** attempts)
|
|
171
|
-
|
|
172
|
-
latency_ms = int((time.time() - start_ts) * 1000)
|
|
173
|
-
|
|
174
|
-
if response.status_code in [200, 201]:
|
|
185
|
+
while True:
|
|
186
|
+
attempts += 1
|
|
175
187
|
try:
|
|
176
|
-
|
|
177
|
-
except
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
"
|
|
186
|
-
|
|
187
|
-
|
|
188
|
+
response = await client.request(method=method, url=url, headers=headers, params=params, json=json_data, auth=auth)
|
|
189
|
+
except (httpx.RequestError, httpx.TimeoutException) as exc:
|
|
190
|
+
logger.warning(f"Bulk request error to {url}: {exc} (attempt {attempts}/{max_attempts})")
|
|
191
|
+
if attempts >= max_attempts:
|
|
192
|
+
raise
|
|
193
|
+
await asyncio.sleep(min(2 ** attempts, 60))
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
if response.status_code == 429:
|
|
197
|
+
retry_after = response.headers.get("Retry-After")
|
|
198
|
+
try:
|
|
199
|
+
wait_seconds = int(retry_after) if retry_after is not None else min(2 ** attempts, 60)
|
|
200
|
+
except Exception:
|
|
201
|
+
wait_seconds = min(2 ** attempts, 60)
|
|
202
|
+
logger.warning(f"HTTP 429 from bulk {url}, retrying after {wait_seconds}s (attempt {attempts}/{max_attempts})")
|
|
203
|
+
if attempts >= max_attempts:
|
|
204
|
+
latency_ms = int((time.time() - start_ts) * 1000)
|
|
205
|
+
return {"success": False, "error": "HTTP 429", "message": response.text, "status_code": 429, "latency_ms": latency_ms, "request_id": request_id}
|
|
206
|
+
await asyncio.sleep(wait_seconds)
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
latency_ms = int((time.time() - start_ts) * 1000)
|
|
210
|
+
|
|
211
|
+
if response.status_code in [200, 201]:
|
|
212
|
+
try:
|
|
213
|
+
return {"success": True, "data": response.json(), "status_code": response.status_code, "latency_ms": latency_ms, "request_id": request_id}
|
|
214
|
+
except BaseException:
|
|
215
|
+
return {"success": True, "data": {"message": "Success"}, "status_code": response.status_code, "latency_ms": latency_ms, "request_id": request_id}
|
|
216
|
+
else:
|
|
217
|
+
logger.warning(f"HTTP {response.status_code} from bulk {url}: {response.text}")
|
|
218
|
+
return {
|
|
219
|
+
"success": False,
|
|
220
|
+
"error": f"HTTP {response.status_code}",
|
|
221
|
+
"message": response.text,
|
|
222
|
+
"status_code": response.status_code,
|
|
223
|
+
"latency_ms": latency_ms,
|
|
224
|
+
"request_id": request_id,
|
|
225
|
+
}
|
|
188
226
|
|
|
189
227
|
except httpx.TimeoutException:
|
|
190
228
|
latency_ms = int((time.time() - start_ts) * 1000)
|
|
@@ -239,7 +277,12 @@ async def test_sienge_connection(_meta: Optional[Dict[str, Any]] = None) -> Dict
|
|
|
239
277
|
|
|
240
278
|
@mcp.tool
|
|
241
279
|
async def get_sienge_customers(
|
|
242
|
-
limit: Optional[int] = 50,
|
|
280
|
+
limit: Optional[int] = 50,
|
|
281
|
+
offset: Optional[int] = 0,
|
|
282
|
+
search: Optional[str] = None,
|
|
283
|
+
customer_type_id: Optional[str] = None,
|
|
284
|
+
fetch_all: Optional[bool] = False,
|
|
285
|
+
max_records: Optional[int] = None,
|
|
243
286
|
) -> Dict:
|
|
244
287
|
"""
|
|
245
288
|
Busca clientes no Sienge com filtros
|
|
@@ -266,6 +309,27 @@ async def get_sienge_customers(
|
|
|
266
309
|
except Exception:
|
|
267
310
|
pass
|
|
268
311
|
|
|
312
|
+
# If caller asked to fetch all, use helper to iterate pages
|
|
313
|
+
if fetch_all:
|
|
314
|
+
items = await _fetch_all_paginated("/customers", params=params, page_size=200, max_records=max_records)
|
|
315
|
+
if isinstance(items, dict) and not items.get("success", True):
|
|
316
|
+
return {"success": False, "error": items.get("error"), "message": items.get("message")}
|
|
317
|
+
|
|
318
|
+
customers = items
|
|
319
|
+
total_count = len(customers)
|
|
320
|
+
response = {
|
|
321
|
+
"success": True,
|
|
322
|
+
"message": f"✅ Encontrados {len(customers)} clientes (fetch_all)",
|
|
323
|
+
"customers": customers,
|
|
324
|
+
"count": len(customers),
|
|
325
|
+
"filters_applied": params,
|
|
326
|
+
}
|
|
327
|
+
try:
|
|
328
|
+
_simple_cache_set(cache_key, response, ttl=30)
|
|
329
|
+
except Exception:
|
|
330
|
+
pass
|
|
331
|
+
return response
|
|
332
|
+
|
|
269
333
|
result = await make_sienge_request("GET", "/customers", params=params)
|
|
270
334
|
|
|
271
335
|
if result["success"]:
|
|
@@ -330,7 +394,13 @@ async def get_sienge_customer_types() -> Dict:
|
|
|
330
394
|
|
|
331
395
|
|
|
332
396
|
@mcp.tool
|
|
333
|
-
async def get_sienge_creditors(
|
|
397
|
+
async def get_sienge_creditors(
|
|
398
|
+
limit: Optional[int] = 50,
|
|
399
|
+
offset: Optional[int] = 0,
|
|
400
|
+
search: Optional[str] = None,
|
|
401
|
+
fetch_all: Optional[bool] = False,
|
|
402
|
+
max_records: Optional[int] = None,
|
|
403
|
+
) -> Dict:
|
|
334
404
|
"""
|
|
335
405
|
Busca credores/fornecedores
|
|
336
406
|
|
|
@@ -343,7 +413,7 @@ async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int]
|
|
|
343
413
|
if search:
|
|
344
414
|
params["search"] = search
|
|
345
415
|
|
|
346
|
-
cache_key = f"creditors:{limit}:{offset}:{search}"
|
|
416
|
+
cache_key = f"creditors:{limit}:{offset}:{search}:{fetch_all}:{max_records}"
|
|
347
417
|
try:
|
|
348
418
|
cached = _simple_cache_get(cache_key)
|
|
349
419
|
if cached:
|
|
@@ -351,6 +421,25 @@ async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int]
|
|
|
351
421
|
except Exception:
|
|
352
422
|
pass
|
|
353
423
|
|
|
424
|
+
# Support fetching all pages when requested
|
|
425
|
+
if fetch_all:
|
|
426
|
+
items = await _fetch_all_paginated("/creditors", params=params, page_size=200, max_records=max_records)
|
|
427
|
+
if isinstance(items, dict) and not items.get("success", True):
|
|
428
|
+
return {"success": False, "error": items.get("error"), "message": items.get("message")}
|
|
429
|
+
|
|
430
|
+
creditors = items
|
|
431
|
+
response = {
|
|
432
|
+
"success": True,
|
|
433
|
+
"message": f"✅ Encontrados {len(creditors)} credores (fetch_all)",
|
|
434
|
+
"creditors": creditors,
|
|
435
|
+
"count": len(creditors),
|
|
436
|
+
}
|
|
437
|
+
try:
|
|
438
|
+
_simple_cache_set(cache_key, response, ttl=30)
|
|
439
|
+
except Exception:
|
|
440
|
+
pass
|
|
441
|
+
return response
|
|
442
|
+
|
|
354
443
|
result = await make_sienge_request("GET", "/creditors", params=params)
|
|
355
444
|
|
|
356
445
|
if result["success"]:
|
|
@@ -1477,24 +1566,14 @@ async def list_sienge_entities() -> Dict:
|
|
|
1477
1566
|
# ============ PAGINATION E NAVEGAÇÃO ============
|
|
1478
1567
|
|
|
1479
1568
|
|
|
1480
|
-
|
|
1481
|
-
async def get_sienge_data_paginated(
|
|
1569
|
+
async def _get_data_paginated_internal(
|
|
1482
1570
|
entity_type: str,
|
|
1483
1571
|
page: int = 1,
|
|
1484
1572
|
page_size: int = 20,
|
|
1485
1573
|
filters: Optional[Dict[str, Any]] = None,
|
|
1486
1574
|
sort_by: Optional[str] = None
|
|
1487
1575
|
) -> Dict:
|
|
1488
|
-
"""
|
|
1489
|
-
Busca dados do Sienge com paginação avançada - compatível com ChatGPT
|
|
1490
|
-
|
|
1491
|
-
Args:
|
|
1492
|
-
entity_type: Tipo de entidade (customers, creditors, projects, bills, etc.)
|
|
1493
|
-
page: Número da página (começando em 1)
|
|
1494
|
-
page_size: Registros por página (máximo 50)
|
|
1495
|
-
filters: Filtros específicos da entidade
|
|
1496
|
-
sort_by: Campo para ordenação (se suportado)
|
|
1497
|
-
"""
|
|
1576
|
+
"""Função interna para paginação (sem decorador @mcp.tool)"""
|
|
1498
1577
|
page_size = min(page_size, 50)
|
|
1499
1578
|
offset = (page - 1) * page_size
|
|
1500
1579
|
|
|
@@ -1569,8 +1648,34 @@ async def get_sienge_data_paginated(
|
|
|
1569
1648
|
return result
|
|
1570
1649
|
|
|
1571
1650
|
|
|
1572
|
-
@mcp.tool
|
|
1573
|
-
async def
|
|
1651
|
+
@mcp.tool
|
|
1652
|
+
async def get_sienge_data_paginated(
|
|
1653
|
+
entity_type: str,
|
|
1654
|
+
page: int = 1,
|
|
1655
|
+
page_size: int = 20,
|
|
1656
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
1657
|
+
sort_by: Optional[str] = None
|
|
1658
|
+
) -> Dict:
|
|
1659
|
+
"""
|
|
1660
|
+
Busca dados do Sienge com paginação avançada - compatível com ChatGPT
|
|
1661
|
+
|
|
1662
|
+
Args:
|
|
1663
|
+
entity_type: Tipo de entidade (customers, creditors, projects, bills, etc.)
|
|
1664
|
+
page: Número da página (começando em 1)
|
|
1665
|
+
page_size: Registros por página (máximo 50)
|
|
1666
|
+
filters: Filtros específicos da entidade
|
|
1667
|
+
sort_by: Campo para ordenação (se suportado)
|
|
1668
|
+
"""
|
|
1669
|
+
return await _get_data_paginated_internal(
|
|
1670
|
+
entity_type=entity_type,
|
|
1671
|
+
page=page,
|
|
1672
|
+
page_size=page_size,
|
|
1673
|
+
filters=filters,
|
|
1674
|
+
sort_by=sort_by
|
|
1675
|
+
)
|
|
1676
|
+
|
|
1677
|
+
|
|
1678
|
+
async def _search_financial_data_internal(
|
|
1574
1679
|
period_start: str,
|
|
1575
1680
|
period_end: str,
|
|
1576
1681
|
search_type: str = "both",
|
|
@@ -1578,17 +1683,7 @@ async def search_sienge_financial_data(
|
|
|
1578
1683
|
amount_max: Optional[float] = None,
|
|
1579
1684
|
customer_creditor_search: Optional[str] = None
|
|
1580
1685
|
) -> Dict:
|
|
1581
|
-
"""
|
|
1582
|
-
Busca avançada em dados financeiros do Sienge - Contas a Pagar e Receber
|
|
1583
|
-
|
|
1584
|
-
Args:
|
|
1585
|
-
period_start: Data inicial do período (YYYY-MM-DD)
|
|
1586
|
-
period_end: Data final do período (YYYY-MM-DD)
|
|
1587
|
-
search_type: Tipo de busca ("receivable", "payable", "both")
|
|
1588
|
-
amount_min: Valor mínimo (opcional)
|
|
1589
|
-
amount_max: Valor máximo (opcional)
|
|
1590
|
-
customer_creditor_search: Buscar por nome de cliente/credor (opcional)
|
|
1591
|
-
"""
|
|
1686
|
+
"""Função interna para busca financeira (sem decorador @mcp.tool)"""
|
|
1592
1687
|
|
|
1593
1688
|
financial_results = {
|
|
1594
1689
|
"receivable": {"success": False, "data": [], "count": 0, "error": None},
|
|
@@ -1725,11 +1820,37 @@ async def search_sienge_financial_data(
|
|
|
1725
1820
|
|
|
1726
1821
|
|
|
1727
1822
|
@mcp.tool
|
|
1728
|
-
async def
|
|
1823
|
+
async def search_sienge_financial_data(
|
|
1824
|
+
period_start: str,
|
|
1825
|
+
period_end: str,
|
|
1826
|
+
search_type: str = "both",
|
|
1827
|
+
amount_min: Optional[float] = None,
|
|
1828
|
+
amount_max: Optional[float] = None,
|
|
1829
|
+
customer_creditor_search: Optional[str] = None
|
|
1830
|
+
) -> Dict:
|
|
1729
1831
|
"""
|
|
1730
|
-
|
|
1731
|
-
|
|
1832
|
+
Busca avançada em dados financeiros do Sienge - Contas a Pagar e Receber
|
|
1833
|
+
|
|
1834
|
+
Args:
|
|
1835
|
+
period_start: Data inicial do período (YYYY-MM-DD)
|
|
1836
|
+
period_end: Data final do período (YYYY-MM-DD)
|
|
1837
|
+
search_type: Tipo de busca ("receivable", "payable", "both")
|
|
1838
|
+
amount_min: Valor mínimo (opcional)
|
|
1839
|
+
amount_max: Valor máximo (opcional)
|
|
1840
|
+
customer_creditor_search: Buscar por nome de cliente/credor (opcional)
|
|
1732
1841
|
"""
|
|
1842
|
+
return await _search_financial_data_internal(
|
|
1843
|
+
period_start=period_start,
|
|
1844
|
+
period_end=period_end,
|
|
1845
|
+
search_type=search_type,
|
|
1846
|
+
amount_min=amount_min,
|
|
1847
|
+
amount_max=amount_max,
|
|
1848
|
+
customer_creditor_search=customer_creditor_search
|
|
1849
|
+
)
|
|
1850
|
+
|
|
1851
|
+
|
|
1852
|
+
async def _get_dashboard_summary_internal() -> Dict:
|
|
1853
|
+
"""Função interna para dashboard (sem decorador @mcp.tool)"""
|
|
1733
1854
|
|
|
1734
1855
|
# Data atual e períodos
|
|
1735
1856
|
today = datetime.now()
|
|
@@ -1827,6 +1948,301 @@ async def get_sienge_dashboard_summary() -> Dict:
|
|
|
1827
1948
|
}
|
|
1828
1949
|
|
|
1829
1950
|
|
|
1951
|
+
@mcp.tool
|
|
1952
|
+
async def get_sienge_dashboard_summary() -> Dict:
|
|
1953
|
+
"""
|
|
1954
|
+
Obtém um resumo tipo dashboard com informações gerais do Sienge
|
|
1955
|
+
Útil para visão geral rápida do sistema
|
|
1956
|
+
"""
|
|
1957
|
+
return await _get_dashboard_summary_internal()
|
|
1958
|
+
|
|
1959
|
+
|
|
1960
|
+
# ============ SUPABASE QUERY TOOLS ============
|
|
1961
|
+
|
|
1962
|
+
|
|
1963
|
+
@mcp.tool
|
|
1964
|
+
async def query_supabase_database(
|
|
1965
|
+
table_name: str,
|
|
1966
|
+
columns: Optional[str] = "*",
|
|
1967
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
1968
|
+
limit: Optional[int] = 100,
|
|
1969
|
+
order_by: Optional[str] = None,
|
|
1970
|
+
search_term: Optional[str] = None,
|
|
1971
|
+
search_columns: Optional[List[str]] = None
|
|
1972
|
+
) -> Dict:
|
|
1973
|
+
"""
|
|
1974
|
+
Executa queries no banco de dados Supabase para buscar dados das tabelas do Sienge
|
|
1975
|
+
|
|
1976
|
+
Args:
|
|
1977
|
+
table_name: Nome da tabela (customers, creditors, enterprises, purchase_invoices, stock_inventories, accounts_receivable, installment_payments, income_installments)
|
|
1978
|
+
columns: Colunas a retornar (padrão: "*")
|
|
1979
|
+
filters: Filtros WHERE como dict {"campo": "valor"}
|
|
1980
|
+
limit: Limite de registros (padrão: 100, máximo: 1000)
|
|
1981
|
+
order_by: Campo para ordenação (ex: "name", "created_at desc")
|
|
1982
|
+
search_term: Termo de busca para busca textual
|
|
1983
|
+
search_columns: Colunas onde fazer busca textual (se não especificado, usa campos de texto principais)
|
|
1984
|
+
|
|
1985
|
+
Nota: As queries são executadas no schema 'sienge_data' (fixo)
|
|
1986
|
+
"""
|
|
1987
|
+
# Validação de parâmetros
|
|
1988
|
+
if not table_name or not isinstance(table_name, str):
|
|
1989
|
+
return {
|
|
1990
|
+
"success": False,
|
|
1991
|
+
"message": "❌ Nome da tabela é obrigatório e deve ser uma string",
|
|
1992
|
+
"error": "INVALID_TABLE_NAME"
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
if limit is not None and (not isinstance(limit, int) or limit <= 0):
|
|
1996
|
+
return {
|
|
1997
|
+
"success": False,
|
|
1998
|
+
"message": "❌ Limite deve ser um número inteiro positivo",
|
|
1999
|
+
"error": "INVALID_LIMIT"
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
if limit and limit > 1000:
|
|
2003
|
+
limit = 1000 # Aplicar limite máximo
|
|
2004
|
+
|
|
2005
|
+
return await _query_supabase_internal(
|
|
2006
|
+
table_name=table_name,
|
|
2007
|
+
columns=columns,
|
|
2008
|
+
filters=filters,
|
|
2009
|
+
limit=limit,
|
|
2010
|
+
order_by=order_by,
|
|
2011
|
+
search_term=search_term,
|
|
2012
|
+
search_columns=search_columns
|
|
2013
|
+
)
|
|
2014
|
+
|
|
2015
|
+
|
|
2016
|
+
@mcp.tool
|
|
2017
|
+
async def get_supabase_table_info(table_name: Optional[str] = None) -> Dict:
|
|
2018
|
+
"""
|
|
2019
|
+
Obtém informações sobre as tabelas disponíveis no Supabase ou detalhes de uma tabela específica
|
|
2020
|
+
|
|
2021
|
+
Args:
|
|
2022
|
+
table_name: Nome da tabela para obter detalhes (opcional)
|
|
2023
|
+
|
|
2024
|
+
Nota: As tabelas estão no schema 'sienge_data' (fixo)
|
|
2025
|
+
"""
|
|
2026
|
+
if not SUPABASE_AVAILABLE:
|
|
2027
|
+
return {
|
|
2028
|
+
"success": False,
|
|
2029
|
+
"message": "❌ Cliente Supabase não disponível",
|
|
2030
|
+
"error": "SUPABASE_NOT_AVAILABLE"
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
client = _get_supabase_client()
|
|
2034
|
+
if not client:
|
|
2035
|
+
return {
|
|
2036
|
+
"success": False,
|
|
2037
|
+
"message": "❌ Cliente Supabase não configurado",
|
|
2038
|
+
"error": "SUPABASE_NOT_CONFIGURED"
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
# Informações das tabelas disponíveis
|
|
2042
|
+
tables_info = {
|
|
2043
|
+
"customers": {
|
|
2044
|
+
"name": "Clientes",
|
|
2045
|
+
"description": "Clientes cadastrados no Sienge",
|
|
2046
|
+
"columns": ["id", "name", "document", "email", "phone", "customer_type_id", "raw", "updated_at", "last_synced_at", "created_at"],
|
|
2047
|
+
"search_fields": ["name", "document", "email"],
|
|
2048
|
+
"indexes": ["document", "name (trigram)", "updated_at"]
|
|
2049
|
+
},
|
|
2050
|
+
"creditors": {
|
|
2051
|
+
"name": "Credores/Fornecedores",
|
|
2052
|
+
"description": "Fornecedores e credores cadastrados",
|
|
2053
|
+
"columns": ["id", "name", "document", "bank_info", "raw", "updated_at", "last_synced_at", "created_at"],
|
|
2054
|
+
"search_fields": ["name", "document"],
|
|
2055
|
+
"indexes": ["document", "name (trigram)", "updated_at"]
|
|
2056
|
+
},
|
|
2057
|
+
"enterprises": {
|
|
2058
|
+
"name": "Empreendimentos/Obras",
|
|
2059
|
+
"description": "Projetos e obras cadastrados",
|
|
2060
|
+
"columns": ["id", "code", "name", "description", "company_id", "type", "metadata", "raw", "updated_at", "last_synced_at", "created_at"],
|
|
2061
|
+
"search_fields": ["name", "description", "code"],
|
|
2062
|
+
"indexes": ["name (trigram)", "company_id", "updated_at"]
|
|
2063
|
+
},
|
|
2064
|
+
"purchase_invoices": {
|
|
2065
|
+
"name": "Notas Fiscais de Compra",
|
|
2066
|
+
"description": "Notas fiscais de compra",
|
|
2067
|
+
"columns": ["id", "sequential_number", "supplier_id", "company_id", "movement_date", "issue_date", "series", "notes", "raw", "updated_at", "last_synced_at", "created_at"],
|
|
2068
|
+
"search_fields": ["sequential_number", "notes"],
|
|
2069
|
+
"indexes": ["supplier_id", "sequential_number", "updated_at"]
|
|
2070
|
+
},
|
|
2071
|
+
"installment_payments": {
|
|
2072
|
+
"name": "Pagamentos de Parcelas",
|
|
2073
|
+
"description": "Pagamentos efetuados para parcelas",
|
|
2074
|
+
"columns": [
|
|
2075
|
+
"id", "installment_uid", "amount", "payment_date", "method", "raw",
|
|
2076
|
+
"updated_at", "last_synced_at", "created_at"
|
|
2077
|
+
],
|
|
2078
|
+
"search_fields": ["installment_uid"],
|
|
2079
|
+
"indexes": ["payment_date", "installment_uid", "updated_at"]
|
|
2080
|
+
},
|
|
2081
|
+
"income_installments": {
|
|
2082
|
+
"name": "Parcelas de Receita",
|
|
2083
|
+
"description": "Parcelas de contas a receber (busca apenas por valores numéricos)",
|
|
2084
|
+
"columns": [
|
|
2085
|
+
"id", "bill_id", "customer_id", "amount", "due_date", "status", "raw",
|
|
2086
|
+
"updated_at", "last_synced_at", "created_at"
|
|
2087
|
+
],
|
|
2088
|
+
"search_fields": ["bill_id (numérico)", "customer_id (numérico)", "amount (numérico)"],
|
|
2089
|
+
"indexes": ["due_date", "status", "updated_at"],
|
|
2090
|
+
"search_note": "Para buscar nesta tabela, use valores numéricos (ex: '123' para bill_id)"
|
|
2091
|
+
},
|
|
2092
|
+
"stock_inventories": {
|
|
2093
|
+
"name": "Inventário de Estoque",
|
|
2094
|
+
"description": "Inventário e movimentações de estoque",
|
|
2095
|
+
"columns": ["id", "cost_center_id", "resource_id", "inventory", "raw", "updated_at", "last_synced_at", "created_at"],
|
|
2096
|
+
"search_fields": ["cost_center_id", "resource_id"],
|
|
2097
|
+
"indexes": ["cost_center_id", "resource_id"]
|
|
2098
|
+
},
|
|
2099
|
+
"accounts_receivable": {
|
|
2100
|
+
"name": "Contas a Receber",
|
|
2101
|
+
"description": "Contas a receber e movimentações financeiras",
|
|
2102
|
+
"columns": ["id", "bill_id", "customer_id", "amount", "due_date", "payment_date", "raw", "updated_at", "last_synced_at", "created_at"],
|
|
2103
|
+
"search_fields": ["bill_id", "customer_id"],
|
|
2104
|
+
"indexes": ["customer_id", "due_date", "updated_at"]
|
|
2105
|
+
},
|
|
2106
|
+
"sync_meta": {
|
|
2107
|
+
"name": "Metadados de Sincronização",
|
|
2108
|
+
"description": "Controle de sincronização entre Sienge e Supabase",
|
|
2109
|
+
"columns": ["id", "entity_name", "last_synced_at", "last_record_count", "notes", "created_at"],
|
|
2110
|
+
"search_fields": ["entity_name"],
|
|
2111
|
+
"indexes": ["entity_name"]
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
if table_name:
|
|
2116
|
+
if table_name in tables_info:
|
|
2117
|
+
return {
|
|
2118
|
+
"success": True,
|
|
2119
|
+
"message": f"✅ Informações da tabela '{table_name}'",
|
|
2120
|
+
"table_info": tables_info[table_name],
|
|
2121
|
+
"table_name": table_name
|
|
2122
|
+
}
|
|
2123
|
+
else:
|
|
2124
|
+
return {
|
|
2125
|
+
"success": False,
|
|
2126
|
+
"message": f"❌ Tabela '{table_name}' não encontrada",
|
|
2127
|
+
"error": "TABLE_NOT_FOUND",
|
|
2128
|
+
"available_tables": list(tables_info.keys())
|
|
2129
|
+
}
|
|
2130
|
+
else:
|
|
2131
|
+
return {
|
|
2132
|
+
"success": True,
|
|
2133
|
+
"message": f"✅ {len(tables_info)} tabelas disponíveis no Supabase",
|
|
2134
|
+
"schema": SUPABASE_SCHEMA,
|
|
2135
|
+
"tables": tables_info,
|
|
2136
|
+
"usage_examples": {
|
|
2137
|
+
"query_customers": "query_supabase_database('customers', search_term='João')",
|
|
2138
|
+
"query_bills_by_date": "query_supabase_database('bills', filters={'due_date': '2024-01-01'})",
|
|
2139
|
+
"query_enterprises": "query_supabase_database('enterprises', columns='id,name,description', limit=50)"
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
|
|
2144
|
+
@mcp.tool
|
|
2145
|
+
async def search_supabase_data(
|
|
2146
|
+
search_term: str,
|
|
2147
|
+
table_names: Optional[List[str]] = None,
|
|
2148
|
+
limit_per_table: Optional[int] = 20
|
|
2149
|
+
) -> Dict:
|
|
2150
|
+
"""
|
|
2151
|
+
Busca universal em múltiplas tabelas do Supabase
|
|
2152
|
+
|
|
2153
|
+
Args:
|
|
2154
|
+
search_term: Termo de busca
|
|
2155
|
+
table_names: Lista de tabelas para buscar (se não especificado, busca em todas)
|
|
2156
|
+
limit_per_table: Limite de resultados por tabela (padrão: 20)
|
|
2157
|
+
"""
|
|
2158
|
+
# Validação de parâmetros
|
|
2159
|
+
if not search_term or not isinstance(search_term, str):
|
|
2160
|
+
return {
|
|
2161
|
+
"success": False,
|
|
2162
|
+
"message": "❌ Termo de busca é obrigatório e deve ser uma string",
|
|
2163
|
+
"error": "INVALID_SEARCH_TERM"
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
if limit_per_table is not None and (not isinstance(limit_per_table, int) or limit_per_table <= 0):
|
|
2167
|
+
return {
|
|
2168
|
+
"success": False,
|
|
2169
|
+
"message": "❌ Limite por tabela deve ser um número inteiro positivo",
|
|
2170
|
+
"error": "INVALID_LIMIT"
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
# Validar e processar table_names
|
|
2174
|
+
if table_names is not None:
|
|
2175
|
+
if not isinstance(table_names, list):
|
|
2176
|
+
return {
|
|
2177
|
+
"success": False,
|
|
2178
|
+
"message": "❌ table_names deve ser uma lista de strings",
|
|
2179
|
+
"error": "INVALID_TABLE_NAMES"
|
|
2180
|
+
}
|
|
2181
|
+
# Filtrar apenas tabelas válidas
|
|
2182
|
+
valid_tables = ["customers", "creditors", "enterprises", "purchase_invoices",
|
|
2183
|
+
"stock_inventories", "accounts_receivable", "sync_meta",
|
|
2184
|
+
"installment_payments", "income_installments"]
|
|
2185
|
+
table_names = [t for t in table_names if t in valid_tables]
|
|
2186
|
+
if not table_names:
|
|
2187
|
+
return {
|
|
2188
|
+
"success": False,
|
|
2189
|
+
"message": "❌ Nenhuma tabela válida especificada",
|
|
2190
|
+
"error": "NO_VALID_TABLES",
|
|
2191
|
+
"valid_tables": valid_tables
|
|
2192
|
+
}
|
|
2193
|
+
else:
|
|
2194
|
+
table_names = ["customers", "creditors", "enterprises", "installment_payments", "income_installments"]
|
|
2195
|
+
|
|
2196
|
+
results = {}
|
|
2197
|
+
total_found = 0
|
|
2198
|
+
|
|
2199
|
+
for table_name in table_names:
|
|
2200
|
+
try:
|
|
2201
|
+
# Chamar a função interna diretamente
|
|
2202
|
+
result = await _query_supabase_internal(
|
|
2203
|
+
table_name=table_name,
|
|
2204
|
+
search_term=search_term,
|
|
2205
|
+
limit=limit_per_table or 20
|
|
2206
|
+
)
|
|
2207
|
+
|
|
2208
|
+
if result["success"]:
|
|
2209
|
+
results[table_name] = {
|
|
2210
|
+
"count": result["count"],
|
|
2211
|
+
"data": result["data"][:5] if result["count"] > 5 else result["data"], # Limitar preview
|
|
2212
|
+
"has_more": result["count"] > 5
|
|
2213
|
+
}
|
|
2214
|
+
total_found += result["count"]
|
|
2215
|
+
else:
|
|
2216
|
+
results[table_name] = {
|
|
2217
|
+
"error": result.get("error"),
|
|
2218
|
+
"count": 0
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
except Exception as e:
|
|
2222
|
+
results[table_name] = {
|
|
2223
|
+
"error": str(e),
|
|
2224
|
+
"count": 0
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
if total_found > 0:
|
|
2228
|
+
return {
|
|
2229
|
+
"success": True,
|
|
2230
|
+
"message": f"✅ Busca '{search_term}' encontrou {total_found} registros em {len([t for t in results.values() if t.get('count', 0) > 0])} tabelas",
|
|
2231
|
+
"search_term": search_term,
|
|
2232
|
+
"total_found": total_found,
|
|
2233
|
+
"results_by_table": results,
|
|
2234
|
+
"suggestion": "Use query_supabase_database() para buscar especificamente em uma tabela e obter mais resultados"
|
|
2235
|
+
}
|
|
2236
|
+
else:
|
|
2237
|
+
return {
|
|
2238
|
+
"success": False,
|
|
2239
|
+
"message": f"❌ Nenhum resultado encontrado para '{search_term}'",
|
|
2240
|
+
"search_term": search_term,
|
|
2241
|
+
"searched_tables": table_names,
|
|
2242
|
+
"results_by_table": results
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
|
|
1830
2246
|
# ============ UTILITÁRIOS ============
|
|
1831
2247
|
|
|
1832
2248
|
|
|
@@ -1856,6 +2272,179 @@ def _get_auth_info_internal() -> Dict:
|
|
|
1856
2272
|
}
|
|
1857
2273
|
|
|
1858
2274
|
|
|
2275
|
+
def _get_supabase_client() -> Optional[Client]:
|
|
2276
|
+
"""Função interna para obter cliente do Supabase"""
|
|
2277
|
+
if not SUPABASE_AVAILABLE:
|
|
2278
|
+
return None
|
|
2279
|
+
if not SUPABASE_URL or not SUPABASE_SERVICE_ROLE_KEY:
|
|
2280
|
+
return None
|
|
2281
|
+
try:
|
|
2282
|
+
client = create_client(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY)
|
|
2283
|
+
return client
|
|
2284
|
+
except Exception as e:
|
|
2285
|
+
logger.warning(f"Erro ao criar cliente Supabase: {e}")
|
|
2286
|
+
return None
|
|
2287
|
+
|
|
2288
|
+
|
|
2289
|
+
async def _query_supabase_internal(
|
|
2290
|
+
table_name: str,
|
|
2291
|
+
columns: Optional[str] = "*",
|
|
2292
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
2293
|
+
limit: Optional[int] = 100,
|
|
2294
|
+
order_by: Optional[str] = None,
|
|
2295
|
+
search_term: Optional[str] = None,
|
|
2296
|
+
search_columns: Optional[List[str]] = None
|
|
2297
|
+
) -> Dict:
|
|
2298
|
+
"""Função interna para query no Supabase (sem decorador @mcp.tool)"""
|
|
2299
|
+
|
|
2300
|
+
if not SUPABASE_AVAILABLE:
|
|
2301
|
+
return {
|
|
2302
|
+
"success": False,
|
|
2303
|
+
"message": "❌ Cliente Supabase não disponível. Instale: pip install supabase",
|
|
2304
|
+
"error": "SUPABASE_NOT_AVAILABLE"
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
client = _get_supabase_client()
|
|
2308
|
+
if not client:
|
|
2309
|
+
return {
|
|
2310
|
+
"success": False,
|
|
2311
|
+
"message": "❌ Cliente Supabase não configurado. Configure SUPABASE_URL e SUPABASE_SERVICE_ROLE_KEY",
|
|
2312
|
+
"error": "SUPABASE_NOT_CONFIGURED"
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
# Validar tabela
|
|
2316
|
+
valid_tables = [
|
|
2317
|
+
"customers", "creditors", "enterprises", "purchase_invoices",
|
|
2318
|
+
"stock_inventories", "accounts_receivable", "sync_meta",
|
|
2319
|
+
"installment_payments", "income_installments"
|
|
2320
|
+
]
|
|
2321
|
+
|
|
2322
|
+
if table_name not in valid_tables:
|
|
2323
|
+
return {
|
|
2324
|
+
"success": False,
|
|
2325
|
+
"message": f"❌ Tabela '{table_name}' não é válida",
|
|
2326
|
+
"error": "INVALID_TABLE",
|
|
2327
|
+
"valid_tables": valid_tables
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
try:
|
|
2331
|
+
# Construir query sempre usando schema sienge_data
|
|
2332
|
+
schema_client = client.schema(SUPABASE_SCHEMA)
|
|
2333
|
+
query = schema_client.table(table_name).select(columns)
|
|
2334
|
+
|
|
2335
|
+
# Aplicar filtros
|
|
2336
|
+
if filters:
|
|
2337
|
+
for field, value in filters.items():
|
|
2338
|
+
if isinstance(value, str) and "%" in value:
|
|
2339
|
+
# Busca com LIKE
|
|
2340
|
+
query = query.like(field, value)
|
|
2341
|
+
elif isinstance(value, list):
|
|
2342
|
+
# Busca com IN
|
|
2343
|
+
query = query.in_(field, value)
|
|
2344
|
+
else:
|
|
2345
|
+
# Busca exata
|
|
2346
|
+
query = query.eq(field, value)
|
|
2347
|
+
|
|
2348
|
+
# Aplicar busca textual se especificada
|
|
2349
|
+
if search_term and search_columns:
|
|
2350
|
+
# Para busca textual, usar OR entre as colunas
|
|
2351
|
+
search_conditions = []
|
|
2352
|
+
for col in search_columns:
|
|
2353
|
+
search_conditions.append(f"{col}.ilike.%{search_term}%")
|
|
2354
|
+
if search_conditions:
|
|
2355
|
+
query = query.or_(",".join(search_conditions))
|
|
2356
|
+
elif search_term:
|
|
2357
|
+
# Busca padrão baseada na tabela
|
|
2358
|
+
default_search_columns = {
|
|
2359
|
+
"customers": ["name", "document", "email"],
|
|
2360
|
+
"creditors": ["name", "document"],
|
|
2361
|
+
"enterprises": ["name", "description", "code"],
|
|
2362
|
+
"purchase_invoices": ["sequential_number", "notes"],
|
|
2363
|
+
"stock_inventories": ["cost_center_id", "resource_id"],
|
|
2364
|
+
"accounts_receivable": ["bill_id", "customer_id"],
|
|
2365
|
+
"installment_payments": ["installment_uid"],
|
|
2366
|
+
"income_installments": [] # Campos numéricos - sem busca textual
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
search_cols = default_search_columns.get(table_name, ["name"])
|
|
2370
|
+
|
|
2371
|
+
# Se não há colunas de texto para buscar, tentar busca numérica
|
|
2372
|
+
if not search_cols:
|
|
2373
|
+
# Para tabelas com campos numéricos, tentar converter search_term para número
|
|
2374
|
+
try:
|
|
2375
|
+
search_num = int(search_term)
|
|
2376
|
+
# Buscar em campos numéricos comuns
|
|
2377
|
+
numeric_conditions = []
|
|
2378
|
+
if table_name == "income_installments":
|
|
2379
|
+
numeric_conditions = [
|
|
2380
|
+
f"bill_id.eq.{search_num}",
|
|
2381
|
+
f"customer_id.eq.{search_num}",
|
|
2382
|
+
f"amount.eq.{search_num}"
|
|
2383
|
+
]
|
|
2384
|
+
elif table_name == "installment_payments":
|
|
2385
|
+
numeric_conditions = [
|
|
2386
|
+
f"installment_uid.eq.{search_num}",
|
|
2387
|
+
f"amount.eq.{search_num}"
|
|
2388
|
+
]
|
|
2389
|
+
|
|
2390
|
+
if numeric_conditions:
|
|
2391
|
+
query = query.or_(",".join(numeric_conditions))
|
|
2392
|
+
except ValueError:
|
|
2393
|
+
# Se não é número, não fazer busca
|
|
2394
|
+
pass
|
|
2395
|
+
else:
|
|
2396
|
+
# Busca textual normal
|
|
2397
|
+
search_conditions = [f"{col}.ilike.%{search_term}%" for col in search_cols]
|
|
2398
|
+
if search_conditions:
|
|
2399
|
+
query = query.or_(",".join(search_conditions))
|
|
2400
|
+
|
|
2401
|
+
# Aplicar ordenação
|
|
2402
|
+
if order_by:
|
|
2403
|
+
if " desc" in order_by.lower():
|
|
2404
|
+
field = order_by.replace(" desc", "").replace(" DESC", "")
|
|
2405
|
+
query = query.order(field, desc=True)
|
|
2406
|
+
else:
|
|
2407
|
+
field = order_by.replace(" asc", "").replace(" ASC", "")
|
|
2408
|
+
query = query.order(field)
|
|
2409
|
+
|
|
2410
|
+
# Aplicar limite
|
|
2411
|
+
limit = min(limit or 100, 1000)
|
|
2412
|
+
query = query.limit(limit)
|
|
2413
|
+
|
|
2414
|
+
# Executar query
|
|
2415
|
+
result = query.execute()
|
|
2416
|
+
|
|
2417
|
+
if hasattr(result, 'data'):
|
|
2418
|
+
data = result.data
|
|
2419
|
+
else:
|
|
2420
|
+
data = result
|
|
2421
|
+
|
|
2422
|
+
return {
|
|
2423
|
+
"success": True,
|
|
2424
|
+
"message": f"✅ Query executada com sucesso na tabela '{table_name}'",
|
|
2425
|
+
"table_name": table_name,
|
|
2426
|
+
"data": data,
|
|
2427
|
+
"count": len(data) if isinstance(data, list) else 1,
|
|
2428
|
+
"query_info": {
|
|
2429
|
+
"columns": columns,
|
|
2430
|
+
"filters": filters,
|
|
2431
|
+
"limit": limit,
|
|
2432
|
+
"order_by": order_by,
|
|
2433
|
+
"search_term": search_term,
|
|
2434
|
+
"search_columns": search_columns
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
except Exception as e:
|
|
2439
|
+
logger.error(f"Erro na query Supabase: {e}")
|
|
2440
|
+
return {
|
|
2441
|
+
"success": False,
|
|
2442
|
+
"message": f"❌ Erro ao executar query na tabela '{table_name}'",
|
|
2443
|
+
"error": str(e),
|
|
2444
|
+
"table_name": table_name
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
|
|
1859
2448
|
# ============ SIMPLE ASYNC CACHE (in-memory, process-local) ============
|
|
1860
2449
|
# Lightweight helper to improve hit-rate on repeated test queries
|
|
1861
2450
|
_SIMPLE_CACHE: Dict[str, Dict[str, Any]] = {}
|
|
@@ -1877,6 +2466,68 @@ def _simple_cache_get(key: str) -> Optional[Dict[str, Any]]:
|
|
|
1877
2466
|
return item.get("value")
|
|
1878
2467
|
|
|
1879
2468
|
|
|
2469
|
+
async def _fetch_all_paginated(
|
|
2470
|
+
endpoint: str,
|
|
2471
|
+
params: Optional[Dict[str, Any]] = None,
|
|
2472
|
+
page_size: int = 200,
|
|
2473
|
+
max_records: Optional[int] = None,
|
|
2474
|
+
results_key: str = "results",
|
|
2475
|
+
use_bulk: bool = False,
|
|
2476
|
+
) -> List[Dict[str, Any]]:
|
|
2477
|
+
"""
|
|
2478
|
+
Helper to fetch all pages from a paginated endpoint that uses limit/offset.
|
|
2479
|
+
|
|
2480
|
+
- endpoint: API endpoint path (starting with /)
|
|
2481
|
+
- params: base params (function will add/override limit and offset)
|
|
2482
|
+
- page_size: maximum per request (API typically allows up to 200)
|
|
2483
|
+
- max_records: optional soft limit to stop early
|
|
2484
|
+
- results_key: key in the JSON response where the array is located (default: 'results')
|
|
2485
|
+
- use_bulk: if True expect bulk-data style response where items may be under 'data'
|
|
2486
|
+
"""
|
|
2487
|
+
params = dict(params or {})
|
|
2488
|
+
all_items: List[Dict[str, Any]] = []
|
|
2489
|
+
offset = int(params.get("offset", 0) or 0)
|
|
2490
|
+
page_size = min(int(page_size), 200)
|
|
2491
|
+
|
|
2492
|
+
while True:
|
|
2493
|
+
params["limit"] = page_size
|
|
2494
|
+
params["offset"] = offset
|
|
2495
|
+
|
|
2496
|
+
# choose the correct requester
|
|
2497
|
+
requester = make_sienge_bulk_request if use_bulk else make_sienge_request
|
|
2498
|
+
result = await requester("GET", endpoint, params=params)
|
|
2499
|
+
|
|
2500
|
+
if not result.get("success"):
|
|
2501
|
+
# stop and raise or return whatever accumulated
|
|
2502
|
+
return {"success": False, "error": result.get("error"), "message": result.get("message")}
|
|
2503
|
+
|
|
2504
|
+
data = result.get("data")
|
|
2505
|
+
|
|
2506
|
+
if use_bulk:
|
|
2507
|
+
items = data.get("data", []) if isinstance(data, dict) else data
|
|
2508
|
+
else:
|
|
2509
|
+
items = data.get(results_key, []) if isinstance(data, dict) else data
|
|
2510
|
+
|
|
2511
|
+
if not isinstance(items, list):
|
|
2512
|
+
# if API returned single object or unexpected structure, append and stop
|
|
2513
|
+
all_items.append(items)
|
|
2514
|
+
break
|
|
2515
|
+
|
|
2516
|
+
all_items.extend(items)
|
|
2517
|
+
|
|
2518
|
+
# enforce max_records if provided
|
|
2519
|
+
if max_records and len(all_items) >= int(max_records):
|
|
2520
|
+
return all_items[: int(max_records)]
|
|
2521
|
+
|
|
2522
|
+
# if fewer items returned than page_size, we've reached the end
|
|
2523
|
+
if len(items) < page_size:
|
|
2524
|
+
break
|
|
2525
|
+
|
|
2526
|
+
offset += len(items) if len(items) > 0 else page_size
|
|
2527
|
+
|
|
2528
|
+
return all_items
|
|
2529
|
+
|
|
2530
|
+
|
|
1880
2531
|
@mcp.tool
|
|
1881
2532
|
def get_auth_info() -> Dict:
|
|
1882
2533
|
"""Retorna informações sobre a configuração de autenticação"""
|
|
@@ -55,7 +55,7 @@ class SiengeLogger:
|
|
|
55
55
|
|
|
56
56
|
# Create formatter
|
|
57
57
|
formatter = logging.Formatter(
|
|
58
|
-
"[%(asctime)s] [PID:%(process)d] %(levelname)s [%(name)s]: %(message)s", datefmt="%Y-%m-%
|
|
58
|
+
"[%(asctime)s] [PID:%(process)d] %(levelname)s [%(name)s]: %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
|
|
59
59
|
)
|
|
60
60
|
file_handler.setFormatter(formatter)
|
|
61
61
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/src/sienge_ecbiesek_mcp.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|