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.

Files changed (22) hide show
  1. {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/PKG-INFO +34 -3
  2. {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/README.md +31 -1
  3. {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/pyproject.toml +3 -2
  4. {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/requirements.txt +3 -0
  5. {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/src/sienge_ecbiesek_mcp.egg-info/PKG-INFO +34 -3
  6. {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/src/sienge_ecbiesek_mcp.egg-info/requires.txt +1 -0
  7. {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/src/sienge_mcp/server.py +748 -97
  8. {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/src/sienge_mcp/utils/logger.py +1 -1
  9. {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/COMO_TESTAR.md +0 -0
  10. {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/GUIA_ATUALIZACAO.md +0 -0
  11. {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/LICENSE +0 -0
  12. {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/MANIFEST.in +0 -0
  13. {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/PUBLICADO_PYPI.md +0 -0
  14. {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/claude_desktop_config.template.json +0 -0
  15. {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/setup.cfg +0 -0
  16. {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/src/sienge_ecbiesek_mcp.egg-info/SOURCES.txt +0 -0
  17. {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/src/sienge_ecbiesek_mcp.egg-info/dependency_links.txt +0 -0
  18. {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/src/sienge_ecbiesek_mcp.egg-info/entry_points.txt +0 -0
  19. {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/src/sienge_ecbiesek_mcp.egg-info/top_level.txt +0 -0
  20. {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/src/sienge_mcp/__init__.py +0 -0
  21. {sienge_ecbiesek_mcp-1.2.3 → sienge_ecbiesek_mcp-1.3.0}/src/sienge_mcp/metadata.py +0 -0
  22. {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.2.3
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 operations.
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.1.0 (Atual)
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.1.0 (Atual)
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.2.3"
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 operations."
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]
@@ -8,6 +8,9 @@ requests>=2.31.0
8
8
  fastapi>=0.100.0
9
9
  uvicorn>=0.31.0
10
10
 
11
+ # Dependências para integração com Supabase
12
+ supabase>=2.0.0
13
+
11
14
  # Dependências opcionais para desenvolvimento
12
15
  # pytest>=7.0.0 # Para testes
13
16
  # black>=23.0.0 # Para formatação de código
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sienge-ecbiesek-mcp
3
- Version: 1.2.3
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 operations.
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.1.0 (Atual)
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
@@ -2,6 +2,7 @@ fastmcp>=2.12.3
2
2
  httpx>=0.25.0
3
3
  pydantic>=2.0.0
4
4
  python-dotenv>=1.0.0
5
+ supabase>=2.0.0
5
6
 
6
7
  [dev]
7
8
  pytest>=7.0.0
@@ -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
- # Retry strategy: prefer tenacity if available
79
- if TENACITY_AVAILABLE:
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
- return {"success": True, "data": response.json(), "status_code": response.status_code, "latency_ms": latency_ms, "request_id": request_id}
103
- except BaseException:
104
- return {"success": True, "data": {"message": "Success"}, "status_code": response.status_code, "latency_ms": latency_ms, "request_id": request_id}
105
- else:
106
- return {
107
- "success": False,
108
- "error": f"HTTP {response.status_code}",
109
- "message": response.text,
110
- "status_code": response.status_code,
111
- "latency_ms": latency_ms,
112
- "request_id": request_id,
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
- if TENACITY_AVAILABLE:
157
- 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))):
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
- return {"success": True, "data": response.json(), "status_code": response.status_code, "latency_ms": latency_ms, "request_id": request_id}
177
- except BaseException:
178
- return {"success": True, "data": {"message": "Success"}, "status_code": response.status_code, "latency_ms": latency_ms, "request_id": request_id}
179
- else:
180
- return {
181
- "success": False,
182
- "error": f"HTTP {response.status_code}",
183
- "message": response.text,
184
- "status_code": response.status_code,
185
- "latency_ms": latency_ms,
186
- "request_id": request_id,
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, offset: Optional[int] = 0, search: Optional[str] = None, customer_type_id: Optional[str] = None
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(limit: Optional[int] = 50, offset: Optional[int] = 0, search: Optional[str] = None) -> Dict:
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
- @mcp.tool
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 search_sienge_financial_data(
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 get_sienge_dashboard_summary() -> Dict:
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
- Obtém um resumo tipo dashboard com informações gerais do Sienge
1731
- Útil para visão geral rápida do sistema
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-%dT%H:%M:%S.%fZ"
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