sienge-ecbiesek-mcp 1.2.2__py3-none-any.whl → 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sienge-ecbiesek-mcp might be problematic. Click here for more details.

sienge_mcp/server.py CHANGED
@@ -2,27 +2,37 @@
2
2
  """
3
3
  SIENGE MCP COMPLETO - FastMCP com Autenticação Flexível
4
4
  Suporta Bearer Token e Basic Auth
5
- CORREÇÕES IMPLEMENTADAS:
6
- 1. Separação entre camada MCP e camada de serviços
7
- 2. Alias compatíveis com checklist
8
- 3. Normalização de parâmetros (camelCase + arrays)
9
- 4. Bulk-data assíncrono com polling
10
- 5. Observabilidade mínima (X-Request-ID, cache, logs)
11
- 6. Ajustes de compatibilidade pontuais
12
5
  """
13
6
 
14
7
  from fastmcp import FastMCP
15
8
  import httpx
16
- from typing import Dict, List, Optional, Any, Union
9
+ from typing import Dict, List, Optional, Any
17
10
  import os
18
11
  from dotenv import load_dotenv
19
- from datetime import datetime, timedelta
12
+ from datetime import datetime
13
+ import time
20
14
  import uuid
21
15
  import asyncio
22
- import json
23
- import time
24
- import logging
25
- from functools import wraps
16
+
17
+ # logger
18
+ from .utils.logger import get_logger
19
+ logger = get_logger()
20
+
21
+ # Optional: prefer tenacity for robust retries; linter will warn if not installed but code falls back
22
+ try:
23
+ from tenacity import AsyncRetrying, wait_exponential, stop_after_attempt, retry_if_exception_type # type: ignore
24
+ TENACITY_AVAILABLE = True
25
+ except Exception:
26
+ TENACITY_AVAILABLE = False
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
26
36
 
27
37
  # Carrega as variáveis de ambiente
28
38
  load_dotenv()
@@ -37,273 +47,104 @@ SIENGE_PASSWORD = os.getenv("SIENGE_PASSWORD", "")
37
47
  SIENGE_API_KEY = os.getenv("SIENGE_API_KEY", "")
38
48
  REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", "30"))
39
49
 
40
- # Cache simples em memória
41
- _cache = {}
42
- CACHE_TTL = 300 # 5 minutos
43
-
44
- # Configurar logging estruturado
45
- logging.basicConfig(
46
- level=logging.INFO,
47
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
48
- )
49
- logger = logging.getLogger("sienge-mcp")
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
50
54
 
51
55
 
52
56
  class SiengeAPIError(Exception):
53
57
  """Exceção customizada para erros da API do Sienge"""
54
- pass
55
-
56
-
57
- # ============ HELPERS DE NORMALIZAÇÃO E OBSERVABILIDADE ============
58
-
59
- def _camel(s: str) -> str:
60
- """Converte snake_case para camelCase"""
61
- if '_' not in s:
62
- return s
63
- parts = s.split('_')
64
- return parts[0] + ''.join(x.capitalize() for x in parts[1:])
65
-
66
-
67
- def to_query(params: dict) -> dict:
68
- """
69
- Converte parâmetros para query string normalizada:
70
- - snake_case → camelCase
71
- - listas/tuplas → CSV string
72
- - booleanos → 'true'/'false' (minúsculo)
73
- - remove valores None
74
- """
75
- if not params:
76
- return {}
77
-
78
- out = {}
79
- for k, v in params.items():
80
- if v is None:
81
- continue
82
- key = _camel(k)
83
- if isinstance(v, (list, tuple)):
84
- out[key] = ','.join(map(str, v))
85
- elif isinstance(v, bool):
86
- out[key] = 'true' if v else 'false'
87
- else:
88
- out[key] = v
89
- return out
90
-
91
-
92
- def to_camel_json(obj: Any) -> Any:
93
- """
94
- Normaliza payload JSON recursivamente:
95
- - snake_case → camelCase nas chaves
96
- - remove valores None
97
- - mantém estrutura de listas e objetos aninhados
98
- """
99
- if isinstance(obj, dict):
100
- return {_camel(k): to_camel_json(v) for k, v in obj.items() if v is not None}
101
- elif isinstance(obj, list):
102
- return [to_camel_json(x) for x in obj]
103
- else:
104
- return obj
105
-
106
-
107
- def _normalize_url(base_url: str, subdomain: str) -> str:
108
- """Normaliza URL evitando //public/api quando subdomain está vazio"""
109
- if not subdomain or subdomain.strip() == "":
110
- return f"{base_url.rstrip('/')}/public/api"
111
- return f"{base_url.rstrip('/')}/{subdomain.strip()}/public/api"
112
-
113
-
114
- def _parse_numeric_value(value: Any) -> float:
115
- """Sanitiza valores numéricos, lidando com vírgulas decimais"""
116
- if value is None:
117
- return 0.0
118
-
119
- if isinstance(value, (int, float)):
120
- return float(value)
121
-
122
- # Se for string, tentar converter
123
- str_value = str(value).strip()
124
- if not str_value:
125
- return 0.0
126
-
127
- # Trocar vírgula por ponto decimal
128
- str_value = str_value.replace(',', '.')
129
-
130
- try:
131
- return float(str_value)
132
- except (ValueError, TypeError):
133
- logger.warning(f"Não foi possível converter '{value}' para número")
134
- return 0.0
135
-
136
-
137
- def _get_cache_key(endpoint: str, params: dict = None) -> str:
138
- """Gera chave de cache baseada no endpoint e parâmetros"""
139
- cache_params = json.dumps(params or {}, sort_keys=True)
140
- return f"{endpoint}:{hash(cache_params)}"
141
-
142
58
 
143
- def _set_cache(key: str, data: Any) -> None:
144
- """Armazena dados no cache com TTL"""
145
- _cache[key] = {
146
- "data": data,
147
- "timestamp": time.time(),
148
- "ttl": CACHE_TTL
149
- }
150
-
151
-
152
- def _get_cache(key: str) -> Optional[Dict]:
153
- """Recupera dados do cache se ainda válidos - CORRIGIDO: mantém shape original"""
154
- if key not in _cache:
155
- return None
156
-
157
- cached = _cache[key]
158
- if time.time() - cached["timestamp"] > cached["ttl"]:
159
- del _cache[key] # Remove cache expirado
160
- return None
161
-
162
- # cached["data"] já é o "result" completo salvo em _set_cache
163
- result = dict(cached["data"]) # cópia rasa
164
- result["cache"] = {
165
- "hit": True,
166
- "ttl_s": cached["ttl"] - (time.time() - cached["timestamp"])
167
- }
168
- return result
169
-
170
-
171
- def _log_request(method: str, endpoint: str, status_code: int, latency: float, request_id: str) -> None:
172
- """Log estruturado das requisições"""
173
- logger.info(
174
- f"HTTP {method} {endpoint} - Status: {status_code} - "
175
- f"Latency: {latency:.3f}s - RequestID: {request_id}"
176
- )
177
-
178
-
179
- def _extract_items_and_total(resp_data: Any) -> tuple:
180
- """
181
- Extrai items e total count de resposta padronizada da API Sienge
182
- Retorna: (items_list, total_count)
183
- """
184
- items = resp_data.get("results", []) if isinstance(resp_data, dict) else (resp_data or [])
185
- meta = resp_data.get("resultSetMetadata", {}) if isinstance(resp_data, dict) else {}
186
- total = meta.get("count", len(items))
187
- return items, total
59
+ pass
188
60
 
189
61
 
190
62
  async def make_sienge_request(
191
- method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None, use_cache: bool = True
63
+ method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None
192
64
  ) -> Dict:
193
65
  """
194
66
  Função auxiliar para fazer requisições à API do Sienge (v1)
195
67
  Suporta tanto Bearer Token quanto Basic Auth
196
- MELHORADO: Observabilidade, cache, normalização de parâmetros
197
68
  """
198
- start_time = time.time()
199
- req_id = str(uuid.uuid4())
200
-
201
- try:
202
- # Normalizar parâmetros
203
- normalized_params = to_query(params) if params else None
204
-
205
- # Verificar cache para operações GET
206
- cache_key = None
207
- if method.upper() == "GET" and use_cache and endpoint in ["/customer-types", "/creditors", "/customers"]:
208
- cache_key = _get_cache_key(endpoint, normalized_params)
209
- cached_result = _get_cache(cache_key)
210
- if cached_result:
211
- cached_result["request_id"] = req_id
212
- return cached_result
213
-
214
- async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
215
- headers = {
216
- "Content-Type": "application/json",
217
- "Accept": "application/json",
218
- "X-Request-ID": req_id
219
- }
69
+ # Attach a request id and measure latency
70
+ request_id = str(uuid.uuid4())
71
+ start_ts = time.time()
220
72
 
221
- # Configurar autenticação e URL (corrigindo URLs duplas)
222
- auth = None
223
- base_normalized = _normalize_url(SIENGE_BASE_URL, SIENGE_SUBDOMAIN)
73
+ headers = {"Content-Type": "application/json", "Accept": "application/json", "X-Request-Id": request_id}
224
74
 
225
- if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
226
- headers["Authorization"] = f"Bearer {SIENGE_API_KEY}"
227
- url = f"{base_normalized}/v1{endpoint}"
228
- elif SIENGE_USERNAME and SIENGE_PASSWORD:
229
- auth = httpx.BasicAuth(SIENGE_USERNAME, SIENGE_PASSWORD)
230
- url = f"{base_normalized}/v1{endpoint}"
231
- else:
232
- return {
233
- "success": False,
234
- "error": "No Authentication",
235
- "message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env",
236
- "request_id": req_id
237
- }
75
+ # Configurar autenticação e URL
76
+ auth = None
238
77
 
239
- response = await client.request(
240
- method=method,
241
- url=url,
242
- headers=headers,
243
- params=normalized_params,
244
- json=json_data,
245
- auth=auth
246
- )
247
-
248
- latency = time.time() - start_time
249
- _log_request(method, endpoint, response.status_code, latency, req_id)
78
+ if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
79
+ headers["Authorization"] = f"Bearer {SIENGE_API_KEY}"
80
+ url = f"{SIENGE_BASE_URL}/{SIENGE_SUBDOMAIN}/public/api/v1{endpoint}"
81
+ elif SIENGE_USERNAME and SIENGE_PASSWORD:
82
+ auth = httpx.BasicAuth(SIENGE_USERNAME, SIENGE_PASSWORD)
83
+ url = f"{SIENGE_BASE_URL}/{SIENGE_SUBDOMAIN}/public/api/v1{endpoint}"
84
+ else:
85
+ return {
86
+ "success": False,
87
+ "error": "No Authentication",
88
+ "message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env",
89
+ "request_id": request_id,
90
+ }
250
91
 
251
- if response.status_code in [200, 201]:
92
+ async def _do_request(client: httpx.AsyncClient):
93
+ return await client.request(method=method, url=url, headers=headers, params=params, json=json_data, auth=auth)
94
+
95
+ try:
96
+ max_attempts = 5
97
+ attempts = 0
98
+ async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
99
+ while True:
100
+ attempts += 1
252
101
  try:
253
- data = response.json()
254
- result = {
255
- "success": True,
256
- "data": data,
257
- "status_code": response.status_code,
258
- "request_id": req_id,
259
- "latency_ms": round(latency * 1000, 2)
260
- }
261
-
262
- # Armazenar no cache se aplicável
263
- if cache_key and method.upper() == "GET":
264
- _set_cache(cache_key, result)
265
- result["cache"] = {"hit": False, "ttl_s": CACHE_TTL}
266
-
267
- return result
268
-
269
- except Exception:
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}")
270
133
  return {
271
- "success": True,
272
- "data": {"message": "Success"},
134
+ "success": False,
135
+ "error": f"HTTP {response.status_code}",
136
+ "message": response.text,
273
137
  "status_code": response.status_code,
274
- "request_id": req_id,
275
- "latency_ms": round(latency * 1000, 2)
138
+ "latency_ms": latency_ms,
139
+ "request_id": request_id,
276
140
  }
277
- else:
278
- return {
279
- "success": False,
280
- "error": f"HTTP {response.status_code}",
281
- "message": response.text,
282
- "status_code": response.status_code,
283
- "request_id": req_id,
284
- "latency_ms": round(latency * 1000, 2)
285
- }
286
141
 
287
142
  except httpx.TimeoutException:
288
- latency = time.time() - start_time
289
- _log_request(method, endpoint, 408, latency, req_id)
290
- return {
291
- "success": False,
292
- "error": "Timeout",
293
- "message": f"A requisição excedeu o tempo limite de {REQUEST_TIMEOUT}s",
294
- "request_id": req_id,
295
- "latency_ms": round(latency * 1000, 2)
296
- }
143
+ latency_ms = int((time.time() - start_ts) * 1000)
144
+ return {"success": False, "error": "Timeout", "message": f"A requisição excedeu o tempo limite de {REQUEST_TIMEOUT}s", "latency_ms": latency_ms, "request_id": request_id}
297
145
  except Exception as e:
298
- latency = time.time() - start_time
299
- _log_request(method, endpoint, 500, latency, req_id)
300
- return {
301
- "success": False,
302
- "error": str(e),
303
- "message": f"Erro na requisição: {str(e)}",
304
- "request_id": req_id,
305
- "latency_ms": round(latency * 1000, 2)
306
- }
146
+ latency_ms = int((time.time() - start_ts) * 1000)
147
+ return {"success": False, "error": str(e), "message": f"Erro na requisição: {str(e)}", "latency_ms": latency_ms, "request_id": request_id}
307
148
 
308
149
 
309
150
  async def make_sienge_bulk_request(
@@ -312,359 +153,94 @@ async def make_sienge_bulk_request(
312
153
  """
313
154
  Função auxiliar para fazer requisições à API bulk-data do Sienge
314
155
  Suporta tanto Bearer Token quanto Basic Auth
315
- MELHORADO: Observabilidade e normalização de parâmetros
316
156
  """
317
- start_time = time.time()
318
- req_id = str(uuid.uuid4())
319
-
320
- try:
321
- # Normalizar parâmetros
322
- normalized_params = to_query(params) if params else None
323
-
324
- async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
325
- headers = {
326
- "Content-Type": "application/json",
327
- "Accept": "application/json",
328
- "X-Request-ID": req_id
329
- }
157
+ # Similar to make_sienge_request but targeting bulk-data endpoints
158
+ request_id = str(uuid.uuid4())
159
+ start_ts = time.time()
330
160
 
331
- # Configurar autenticação e URL para bulk-data (corrigindo URLs duplas)
332
- auth = None
333
- base_normalized = _normalize_url(SIENGE_BASE_URL, SIENGE_SUBDOMAIN)
161
+ headers = {"Content-Type": "application/json", "Accept": "application/json", "X-Request-Id": request_id}
334
162
 
335
- if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
336
- headers["Authorization"] = f"Bearer {SIENGE_API_KEY}"
337
- url = f"{base_normalized}/bulk-data/v1{endpoint}"
338
- elif SIENGE_USERNAME and SIENGE_PASSWORD:
339
- auth = httpx.BasicAuth(SIENGE_USERNAME, SIENGE_PASSWORD)
340
- url = f"{base_normalized}/bulk-data/v1{endpoint}"
341
- else:
342
- return {
343
- "success": False,
344
- "error": "No Authentication",
345
- "message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env",
346
- "request_id": req_id
347
- }
163
+ auth = None
164
+ if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
165
+ headers["Authorization"] = f"Bearer {SIENGE_API_KEY}"
166
+ url = f"{SIENGE_BASE_URL}/{SIENGE_SUBDOMAIN}/public/api/bulk-data/v1{endpoint}"
167
+ elif SIENGE_USERNAME and SIENGE_PASSWORD:
168
+ auth = httpx.BasicAuth(SIENGE_USERNAME, SIENGE_PASSWORD)
169
+ url = f"{SIENGE_BASE_URL}/{SIENGE_SUBDOMAIN}/public/api/bulk-data/v1{endpoint}"
170
+ else:
171
+ return {
172
+ "success": False,
173
+ "error": "No Authentication",
174
+ "message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env",
175
+ "request_id": request_id,
176
+ }
348
177
 
349
- response = await client.request(
350
- method=method,
351
- url=url,
352
- headers=headers,
353
- params=normalized_params,
354
- json=json_data,
355
- auth=auth
356
- )
357
-
358
- latency = time.time() - start_time
359
- _log_request(method, f"bulk-data{endpoint}", response.status_code, latency, req_id)
178
+ async def _do_request(client: httpx.AsyncClient):
179
+ return await client.request(method=method, url=url, headers=headers, params=params, json=json_data, auth=auth)
360
180
 
361
- if response.status_code in [200, 201, 202]:
181
+ try:
182
+ max_attempts = 5
183
+ attempts = 0
184
+ async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
185
+ while True:
186
+ attempts += 1
362
187
  try:
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}")
363
218
  return {
364
- "success": True,
365
- "data": response.json(),
219
+ "success": False,
220
+ "error": f"HTTP {response.status_code}",
221
+ "message": response.text,
366
222
  "status_code": response.status_code,
367
- "request_id": req_id,
368
- "latency_ms": round(latency * 1000, 2)
223
+ "latency_ms": latency_ms,
224
+ "request_id": request_id,
369
225
  }
370
- except Exception:
371
- return {
372
- "success": True,
373
- "data": {"message": "Success"},
374
- "status_code": response.status_code,
375
- "request_id": req_id,
376
- "latency_ms": round(latency * 1000, 2)
377
- }
378
- else:
379
- return {
380
- "success": False,
381
- "error": f"HTTP {response.status_code}",
382
- "message": response.text,
383
- "status_code": response.status_code,
384
- "request_id": req_id,
385
- "latency_ms": round(latency * 1000, 2)
386
- }
387
226
 
388
227
  except httpx.TimeoutException:
389
- latency = time.time() - start_time
390
- _log_request(method, f"bulk-data{endpoint}", 408, latency, req_id)
391
- return {
392
- "success": False,
393
- "error": "Timeout",
394
- "message": f"A requisição excedeu o tempo limite de {REQUEST_TIMEOUT}s",
395
- "request_id": req_id,
396
- "latency_ms": round(latency * 1000, 2)
397
- }
228
+ latency_ms = int((time.time() - start_ts) * 1000)
229
+ return {"success": False, "error": "Timeout", "message": f"A requisição excedeu o tempo limite de {REQUEST_TIMEOUT}s", "latency_ms": latency_ms, "request_id": request_id}
398
230
  except Exception as e:
399
- latency = time.time() - start_time
400
- _log_request(method, f"bulk-data{endpoint}", 500, latency, req_id)
401
- return {
402
- "success": False,
403
- "error": str(e),
404
- "message": f"Erro na requisição bulk-data: {str(e)}",
405
- "request_id": req_id,
406
- "latency_ms": round(latency * 1000, 2)
407
- }
408
-
409
-
410
- async def _fetch_bulk_with_polling(method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None) -> Dict:
411
- """
412
- Faz requisição bulk com polling automático para requests assíncronos (202)
413
- """
414
- correlation_id = str(uuid.uuid4())
415
-
416
- # Fazer requisição inicial
417
- result = await make_sienge_bulk_request(method, endpoint, params, json_data)
418
-
419
- # Se não foi 202 ou não tem identifier, retornar resultado direto
420
- if result.get("status_code") != 202:
421
- return result
422
-
423
- data = result.get("data", {})
424
- if not isinstance(data, dict) or not data.get("identifier"):
425
- return result
426
-
427
- # Processar requisição assíncrona com polling
428
- identifier = data["identifier"]
429
- request_id = result.get("request_id")
430
-
431
- logger.info(f"Iniciando polling para bulk request - Identifier: {identifier} - RequestID: {request_id}")
432
-
433
- max_attempts = 30 # Máximo 5 minutos (30 * 10s)
434
- attempt = 0
435
- backoff_delay = 2 # Começar com 2 segundos
436
-
437
- while attempt < max_attempts:
438
- attempt += 1
439
- await asyncio.sleep(backoff_delay)
440
-
441
- # Verificar status do processamento
442
- status_result = await make_sienge_bulk_request("GET", f"/async/{identifier}")
443
-
444
- if not status_result["success"]:
445
- logger.error(f"Erro ao verificar status do bulk request {identifier}: {status_result.get('error')}")
446
- break
447
-
448
- status_data = status_result.get("data", {})
449
- status = status_data.get("status", "unknown")
450
-
451
- logger.info(f"Polling attempt {attempt} - Status: {status} - Identifier: {identifier}")
452
-
453
- if status == "completed":
454
- # Buscar resultados finais
455
- all_chunks = []
456
- chunk_count = status_data.get("chunk_count", 1)
457
-
458
- chunks_downloaded = 0
459
- for chunk_num in range(chunk_count):
460
- try:
461
- # CORRIGIDO: endpoint aninhado sob /async
462
- chunk_result = await make_sienge_bulk_request("GET", f"/async/{identifier}/result/{chunk_num}")
463
- if chunk_result["success"]:
464
- chunk_data = chunk_result.get("data", {}).get("data", [])
465
- if isinstance(chunk_data, list):
466
- all_chunks.extend(chunk_data)
467
- chunks_downloaded += 1
468
- except Exception as e:
469
- logger.warning(f"Erro ao buscar chunk {chunk_num}: {e}")
470
-
471
- return {
472
- "success": True,
473
- "data": all_chunks,
474
- "async_identifier": identifier,
475
- "correlation_id": correlation_id,
476
- "chunks_downloaded": chunks_downloaded,
477
- "rows_returned": len(all_chunks),
478
- "polling_attempts": attempt,
479
- "request_id": request_id
480
- }
481
-
482
- elif status == "failed" or status == "error":
483
- return {
484
- "success": False,
485
- "error": "Bulk processing failed",
486
- "message": status_data.get("error_message", "Processamento bulk falhou"),
487
- "async_identifier": identifier,
488
- "correlation_id": correlation_id,
489
- "polling_attempts": attempt,
490
- "request_id": request_id
491
- }
492
-
493
- # Aumentar delay progressivamente (backoff exponencial limitado)
494
- backoff_delay = min(backoff_delay * 1.5, 10) # Máximo 10 segundos
495
-
496
- # Timeout do polling
497
- return {
498
- "success": False,
499
- "error": "Polling timeout",
500
- "message": f"Processamento bulk não completou em {max_attempts} tentativas",
501
- "async_identifier": identifier,
502
- "correlation_id": correlation_id,
503
- "polling_attempts": attempt,
504
- "request_id": request_id
505
- }
506
-
507
-
508
- # ============ CAMADA DE SERVIÇOS (FUNÇÕES INTERNAS) ============
509
-
510
- async def _svc_get_customer_types() -> Dict:
511
- """Serviço interno: buscar tipos de clientes"""
512
- return await make_sienge_request("GET", "/customer-types", use_cache=True)
513
-
514
-
515
- async def _svc_get_customers(*, limit: int = 50, offset: int = 0, search: str = None, customer_type_id: str = None) -> Dict:
516
- """Serviço interno: buscar clientes"""
517
- params = {"limit": min(limit or 50, 200), "offset": offset or 0}
518
- if search:
519
- params["search"] = search
520
- if customer_type_id:
521
- params["customer_type_id"] = customer_type_id
522
-
523
- return await make_sienge_request("GET", "/customers", params=params)
524
-
525
-
526
- async def _svc_get_creditors(*, limit: int = 50, offset: int = 0, search: str = None) -> Dict:
527
- """Serviço interno: buscar credores/fornecedores"""
528
- params = {"limit": min(limit or 50, 200), "offset": offset or 0}
529
- if search:
530
- params["search"] = search
531
-
532
- return await make_sienge_request("GET", "/creditors", params=params)
533
-
534
-
535
- async def _svc_get_creditor_bank_info(*, creditor_id: str) -> Dict:
536
- """Serviço interno: informações bancárias de credor"""
537
- return await make_sienge_request("GET", f"/creditors/{creditor_id}/bank-informations")
538
-
539
-
540
- async def _svc_get_projects(*, limit: int = 100, offset: int = 0, company_id: int = None,
541
- enterprise_type: int = None, receivable_register: str = None,
542
- only_buildings_enabled: bool = False) -> Dict:
543
- """Serviço interno: buscar empreendimentos/projetos"""
544
- params = {"limit": min(limit or 100, 200), "offset": offset or 0}
545
-
546
- if company_id:
547
- params["company_id"] = company_id
548
- if enterprise_type:
549
- params["type"] = enterprise_type
550
- if receivable_register:
551
- params["receivable_register"] = receivable_register
552
- if only_buildings_enabled:
553
- params["only_buildings_enabled_for_integration"] = only_buildings_enabled
554
-
555
- return await make_sienge_request("GET", "/enterprises", params=params)
556
-
557
-
558
- async def _svc_get_bills(*, start_date: str = None, end_date: str = None, creditor_id: str = None,
559
- status: str = None, limit: int = 50) -> Dict:
560
- """Serviço interno: buscar títulos a pagar"""
561
- # Se start_date não fornecido, usar últimos 30 dias
562
- if not start_date:
563
- start_date = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
564
-
565
- # Se end_date não fornecido, usar hoje
566
- if not end_date:
567
- end_date = datetime.now().strftime("%Y-%m-%d")
568
-
569
- params = {"start_date": start_date, "end_date": end_date, "limit": min(limit or 50, 200)}
570
-
571
- if creditor_id:
572
- params["creditor_id"] = creditor_id
573
- if status:
574
- params["status"] = status
575
-
576
- return await make_sienge_request("GET", "/bills", params=params)
577
-
578
-
579
- async def _svc_get_accounts_receivable(*, start_date: str, end_date: str, selection_type: str = "D",
580
- company_id: int = None, cost_centers_id: List[int] = None,
581
- correction_indexer_id: int = None, correction_date: str = None,
582
- change_start_date: str = None, completed_bills: str = None,
583
- origins_ids: List[str] = None, bearers_id_in: List[int] = None,
584
- bearers_id_not_in: List[int] = None) -> Dict:
585
- """Serviço interno: buscar contas a receber via bulk-data"""
586
- params = {"start_date": start_date, "end_date": end_date, "selection_type": selection_type}
587
-
588
- if company_id:
589
- params["company_id"] = company_id
590
- if cost_centers_id:
591
- params["cost_centers_id"] = cost_centers_id
592
- if correction_indexer_id:
593
- params["correction_indexer_id"] = correction_indexer_id
594
- if correction_date:
595
- params["correction_date"] = correction_date
596
- if change_start_date:
597
- params["change_start_date"] = change_start_date
598
- if completed_bills:
599
- params["completed_bills"] = completed_bills
600
- if origins_ids:
601
- params["origins_ids"] = origins_ids
602
- if bearers_id_in:
603
- params["bearers_id_in"] = bearers_id_in
604
- if bearers_id_not_in:
605
- params["bearers_id_not_in"] = bearers_id_not_in
606
-
607
- return await _fetch_bulk_with_polling("GET", "/income", params=params)
608
-
609
-
610
- async def _svc_get_accounts_receivable_by_bills(*, bills_ids: List[int], correction_indexer_id: int = None,
611
- correction_date: str = None) -> Dict:
612
- """Serviço interno: buscar contas a receber por títulos específicos"""
613
- params = {"bills_ids": bills_ids}
614
-
615
- if correction_indexer_id:
616
- params["correction_indexer_id"] = correction_indexer_id
617
- if correction_date:
618
- params["correction_date"] = correction_date
619
-
620
- return await _fetch_bulk_with_polling("GET", "/income/by-bills", params=params)
621
-
622
-
623
- async def _svc_get_purchase_orders(*, purchase_order_id: str = None, status: str = None,
624
- date_from: str = None, limit: int = 50) -> Dict:
625
- """Serviço interno: buscar pedidos de compra"""
626
- if purchase_order_id:
627
- return await make_sienge_request("GET", f"/purchase-orders/{purchase_order_id}")
628
-
629
- params = {"limit": min(limit or 50, 200)}
630
- if status:
631
- params["status"] = status
632
- if date_from:
633
- params["date_from"] = date_from
634
-
635
- return await make_sienge_request("GET", "/purchase-orders", params=params)
636
-
637
-
638
- async def _svc_get_purchase_requests(*, purchase_request_id: str = None, limit: int = 50, status: str = None) -> Dict:
639
- """Serviço interno: buscar solicitações de compra"""
640
- if purchase_request_id:
641
- return await make_sienge_request("GET", f"/purchase-requests/{purchase_request_id}")
642
-
643
- params = {"limit": min(limit or 50, 200)}
644
- if status:
645
- params["status"] = status
646
-
647
- return await make_sienge_request("GET", "/purchase-requests", params=params)
648
-
649
-
650
- async def _svc_get_purchase_invoices(*, limit: int = 50, date_from: str = None) -> Dict:
651
- """Serviço interno: listar notas fiscais de compra"""
652
- params = {"limit": min(limit or 50, 200)}
653
- if date_from:
654
- params["date_from"] = date_from
655
-
656
- return await make_sienge_request("GET", "/purchase-invoices", params=params)
231
+ latency_ms = int((time.time() - start_ts) * 1000)
232
+ return {"success": False, "error": str(e), "message": f"Erro na requisição bulk-data: {str(e)}", "latency_ms": latency_ms, "request_id": request_id}
657
233
 
658
234
 
659
235
  # ============ CONEXÃO E TESTE ============
660
236
 
661
237
 
662
238
  @mcp.tool
663
- async def test_sienge_connection(_meta: Optional[Dict] = None) -> Dict:
664
- """Testa a conexão com a API do Sienge"""
239
+ async def test_sienge_connection(_meta: Optional[Dict[str, Any]] = None) -> Dict:
240
+ """Testa a conexão com a API do Sienge e retorna métricas básicas"""
665
241
  try:
666
- # Usar serviço interno
667
- result = await _svc_get_customer_types()
242
+ # Tentar endpoint mais simples primeiro
243
+ result = await make_sienge_request("GET", "/customer-types")
668
244
 
669
245
  if result["success"]:
670
246
  auth_method = "Bearer Token" if SIENGE_API_KEY else "Basic Auth"
@@ -674,9 +250,8 @@ async def test_sienge_connection(_meta: Optional[Dict] = None) -> Dict:
674
250
  "api_status": "Online",
675
251
  "auth_method": auth_method,
676
252
  "timestamp": datetime.now().isoformat(),
677
- "request_id": result.get("request_id"),
678
253
  "latency_ms": result.get("latency_ms"),
679
- "cache": result.get("cache")
254
+ "request_id": result.get("request_id"),
680
255
  }
681
256
  else:
682
257
  return {
@@ -685,7 +260,8 @@ async def test_sienge_connection(_meta: Optional[Dict] = None) -> Dict:
685
260
  "error": result.get("error"),
686
261
  "details": result.get("message"),
687
262
  "timestamp": datetime.now().isoformat(),
688
- "request_id": result.get("request_id")
263
+ "latency_ms": result.get("latency_ms"),
264
+ "request_id": result.get("request_id"),
689
265
  }
690
266
  except Exception as e:
691
267
  return {
@@ -701,8 +277,12 @@ async def test_sienge_connection(_meta: Optional[Dict] = None) -> Dict:
701
277
 
702
278
  @mcp.tool
703
279
  async def get_sienge_customers(
704
- limit: Optional[int] = 50, offset: Optional[int] = 0, search: Optional[str] = None, customer_type_id: Optional[str] = None,
705
- _meta: Optional[Dict] = 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,
706
286
  ) -> Dict:
707
287
  """
708
288
  Busca clientes no Sienge com filtros
@@ -713,13 +293,44 @@ async def get_sienge_customers(
713
293
  search: Buscar por nome ou documento
714
294
  customer_type_id: Filtrar por tipo de cliente
715
295
  """
716
- # Usar serviço interno
717
- result = await _svc_get_customers(
718
- limit=limit or 50,
719
- offset=offset or 0,
720
- search=search,
721
- customer_type_id=customer_type_id
722
- )
296
+ params = {"limit": min(limit or 50, 200), "offset": offset or 0}
297
+
298
+ if search:
299
+ params["search"] = search
300
+ if customer_type_id:
301
+ params["customer_type_id"] = customer_type_id
302
+
303
+ # Basic in-memory cache for lightweight GETs
304
+ cache_key = f"customers:{limit}:{offset}:{search}:{customer_type_id}"
305
+ try:
306
+ cached = _simple_cache_get(cache_key)
307
+ if cached:
308
+ return cached
309
+ except Exception:
310
+ pass
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
+
333
+ result = await make_sienge_request("GET", "/customers", params=params)
723
334
 
724
335
  if result["success"]:
725
336
  data = result["data"]
@@ -727,34 +338,31 @@ async def get_sienge_customers(
727
338
  metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
728
339
  total_count = metadata.get("count", len(customers))
729
340
 
730
- return {
341
+ response = {
731
342
  "success": True,
732
343
  "message": f"✅ Encontrados {len(customers)} clientes (total: {total_count})",
733
344
  "customers": customers,
734
345
  "count": len(customers),
735
- "total_count": total_count,
736
- "filters_applied": {
737
- "limit": limit, "offset": offset, "search": search, "customer_type_id": customer_type_id
738
- },
739
- "request_id": result.get("request_id"),
740
- "latency_ms": result.get("latency_ms"),
741
- "cache": result.get("cache")
346
+ "filters_applied": params,
742
347
  }
348
+ try:
349
+ _simple_cache_set(cache_key, response, ttl=30)
350
+ except Exception:
351
+ pass
352
+ return response
743
353
 
744
354
  return {
745
355
  "success": False,
746
356
  "message": "❌ Erro ao buscar clientes",
747
357
  "error": result.get("error"),
748
358
  "details": result.get("message"),
749
- "request_id": result.get("request_id")
750
359
  }
751
360
 
752
361
 
753
362
  @mcp.tool
754
- async def get_sienge_customer_types(_meta: Optional[Dict] = None) -> Dict:
363
+ async def get_sienge_customer_types() -> Dict:
755
364
  """Lista tipos de clientes disponíveis"""
756
- # Usar serviço interno
757
- result = await _svc_get_customer_types()
365
+ result = await make_sienge_request("GET", "/customer-types")
758
366
 
759
367
  if result["success"]:
760
368
  data = result["data"]
@@ -762,211 +370,23 @@ async def get_sienge_customer_types(_meta: Optional[Dict] = None) -> Dict:
762
370
  metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
763
371
  total_count = metadata.get("count", len(customer_types))
764
372
 
765
- return {
373
+ response = {
766
374
  "success": True,
767
375
  "message": f"✅ Encontrados {len(customer_types)} tipos de clientes (total: {total_count})",
768
376
  "customer_types": customer_types,
769
377
  "count": len(customer_types),
770
- "total_count": total_count,
771
- "request_id": result.get("request_id"),
772
- "latency_ms": result.get("latency_ms"),
773
- "cache": result.get("cache")
774
378
  }
775
-
776
- return {
777
- "success": False,
778
- "message": "❌ Erro ao buscar tipos de clientes",
779
- "error": result.get("error"),
780
- "details": result.get("message"),
781
- "request_id": result.get("request_id")
782
- }
783
-
784
-
785
- # ============ ALIAS COMPATÍVEIS COM CHECKLIST ============
786
-
787
- @mcp.tool
788
- async def get_sienge_enterprises(
789
- limit: int = 100, offset: int = 0, company_id: int = None, enterprise_type: int = None,
790
- _meta: Optional[Dict] = None) -> Dict:
791
- """
792
- ALIAS: get_sienge_projects → get_sienge_enterprises
793
- Busca empreendimentos/obras (compatibilidade com checklist)
794
- """
795
- return await get_sienge_projects(
796
- limit=limit,
797
- offset=offset,
798
- company_id=company_id,
799
- enterprise_type=enterprise_type
800
- )
801
-
802
-
803
- @mcp.tool
804
- async def get_sienge_suppliers(
805
- limit: int = 50,
806
- offset: int = 0,
807
- search: str = None,
808
- _meta: Optional[Dict] = None) -> Dict:
809
- """
810
- ALIAS: get_sienge_creditors → get_sienge_suppliers
811
- Busca fornecedores (compatibilidade com checklist)
812
- """
813
- return await get_sienge_creditors(limit=limit, offset=offset, search=search)
814
-
815
-
816
- @mcp.tool
817
- async def search_sienge_finances(
818
- period_start: str,
819
- period_end: str,
820
- account_type: Optional[str] = None,
821
- cost_center: Optional[str] = None, # ignorado por enquanto (não suportado na API atual)
822
- amount_filter: Optional[str] = None,
823
- customer_creditor: Optional[str] = None
824
- ) -> Dict:
825
- """
826
- ALIAS: search_sienge_financial_data → search_sienge_finances
827
- - account_type: receivable | payable | both
828
- - amount_filter: "100..500", ">=1000", "<=500", ">100", "<200", "=750"
829
- - customer_creditor: termo de busca (cliente/credor)
830
- """
831
- # 1) mapear tipo
832
- search_type = (account_type or "both").lower()
833
- if search_type not in {"receivable", "payable", "both"}:
834
- search_type = "both"
835
-
836
- # 2) parse de faixa de valores
837
- amount_min = amount_max = None
838
- if amount_filter:
839
- s = amount_filter.replace(" ", "")
840
379
  try:
841
- if ".." in s:
842
- lo, hi = s.split("..", 1)
843
- amount_min = float(lo) if lo else None
844
- amount_max = float(hi) if hi else None
845
- elif s.startswith(">="):
846
- amount_min = float(s[2:])
847
- elif s.startswith("<="):
848
- amount_max = float(s[2:])
849
- elif s.startswith(">"):
850
- # >x → min = x (estrito não suportado; aproximamos)
851
- amount_min = float(s[1:])
852
- elif s.startswith("<"):
853
- amount_max = float(s[1:])
854
- elif s.startswith("="):
855
- v = float(s[1:])
856
- amount_min = v
857
- amount_max = v
858
- else:
859
- # número puro → min
860
- amount_min = float(s)
861
- except ValueError:
862
- # filtro inválido → ignora silenciosamente
863
- amount_min = amount_max = None
864
-
865
- return await search_sienge_financial_data(
866
- period_start=period_start,
867
- period_end=period_end,
868
- search_type=search_type,
869
- amount_min=amount_min,
870
- amount_max=amount_max,
871
- customer_creditor_search=customer_creditor
872
- )
873
-
874
-
875
- @mcp.tool
876
- async def get_sienge_accounts_payable(
877
- start_date: str = None, end_date: str = None, creditor_id: str = None,
878
- status: str = None, limit: int = 50,
879
- _meta: Optional[Dict] = None) -> Dict:
880
- """
881
- ALIAS: get_sienge_bills → get_sienge_accounts_payable
882
- Busca contas a pagar (compatibilidade com checklist)
883
- """
884
- return await get_sienge_bills(
885
- start_date=start_date,
886
- end_date=end_date,
887
- creditor_id=creditor_id,
888
- status=status,
889
- limit=limit
890
- )
891
-
892
-
893
- @mcp.tool
894
- async def list_sienge_purchase_invoices(limit: int = 50, date_from: str = None,
895
- _meta: Optional[Dict] = None) -> Dict:
896
- """
897
- Lista notas fiscais de compra (versão list/plural esperada pelo checklist)
898
-
899
- Args:
900
- limit: Máximo de registros (padrão: 50, máx: 200)
901
- date_from: Data inicial (YYYY-MM-DD)
902
- """
903
- # Usar serviço interno
904
- result = await _svc_get_purchase_invoices(limit=limit, date_from=date_from)
905
-
906
- if result["success"]:
907
- data = result["data"]
908
- invoices = data.get("results", []) if isinstance(data, dict) else data
909
- metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
910
- total_count = metadata.get("count", len(invoices))
911
-
912
- return {
913
- "success": True,
914
- "message": f"✅ Encontradas {len(invoices)} notas fiscais de compra (total: {total_count})",
915
- "purchase_invoices": invoices,
916
- "count": len(invoices),
917
- "total_count": total_count,
918
- "filters_applied": {"limit": limit, "date_from": date_from},
919
- "request_id": result.get("request_id"),
920
- "latency_ms": result.get("latency_ms"),
921
- "cache": result.get("cache")
922
- }
923
-
924
- return {
925
- "success": False,
926
- "message": "❌ Erro ao buscar notas fiscais de compra",
927
- "error": result.get("error"),
928
- "details": result.get("message"),
929
- "request_id": result.get("request_id")
930
- }
931
-
932
-
933
- @mcp.tool
934
- async def list_sienge_purchase_requests(limit: int = 50, status: str = None,
935
- _meta: Optional[Dict] = None) -> Dict:
936
- """
937
- Lista solicitações de compra (versão list/plural esperada pelo checklist)
938
-
939
- Args:
940
- limit: Máximo de registros (padrão: 50, máx: 200)
941
- status: Status da solicitação
942
- """
943
- # Usar serviço interno
944
- result = await _svc_get_purchase_requests(limit=limit, status=status)
945
-
946
- if result["success"]:
947
- data = result["data"]
948
- requests = data.get("results", []) if isinstance(data, dict) else data
949
- metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
950
- total_count = metadata.get("count", len(requests))
951
-
952
- return {
953
- "success": True,
954
- "message": f"✅ Encontradas {len(requests)} solicitações de compra (total: {total_count})",
955
- "purchase_requests": requests,
956
- "count": len(requests),
957
- "total_count": total_count,
958
- "filters_applied": {"limit": limit, "status": status},
959
- "request_id": result.get("request_id"),
960
- "latency_ms": result.get("latency_ms"),
961
- "cache": result.get("cache")
962
- }
380
+ _simple_cache_set("customer_types", response, ttl=300)
381
+ except Exception:
382
+ pass
383
+ return response
963
384
 
964
385
  return {
965
386
  "success": False,
966
- "message": "❌ Erro ao buscar solicitações de compra",
387
+ "message": "❌ Erro ao buscar tipos de clientes",
967
388
  "error": result.get("error"),
968
389
  "details": result.get("message"),
969
- "request_id": result.get("request_id")
970
390
  }
971
391
 
972
392
 
@@ -974,8 +394,13 @@ async def list_sienge_purchase_requests(limit: int = 50, status: str = None,
974
394
 
975
395
 
976
396
  @mcp.tool
977
- async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int] = 0, search: Optional[str] = None,
978
- _meta: Optional[Dict] = 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:
979
404
  """
980
405
  Busca credores/fornecedores
981
406
 
@@ -984,12 +409,38 @@ async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int]
984
409
  offset: Pular registros (padrão: 0)
985
410
  search: Buscar por nome
986
411
  """
987
- # Usar serviço interno
988
- result = await _svc_get_creditors(
989
- limit=limit or 50,
990
- offset=offset or 0,
991
- search=search
992
- )
412
+ params = {"limit": min(limit or 50, 200), "offset": offset or 0}
413
+ if search:
414
+ params["search"] = search
415
+
416
+ cache_key = f"creditors:{limit}:{offset}:{search}:{fetch_all}:{max_records}"
417
+ try:
418
+ cached = _simple_cache_get(cache_key)
419
+ if cached:
420
+ return cached
421
+ except Exception:
422
+ pass
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
+
443
+ result = await make_sienge_request("GET", "/creditors", params=params)
993
444
 
994
445
  if result["success"]:
995
446
  data = result["data"]
@@ -997,38 +448,35 @@ async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int]
997
448
  metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
998
449
  total_count = metadata.get("count", len(creditors))
999
450
 
1000
- return {
451
+ response = {
1001
452
  "success": True,
1002
453
  "message": f"✅ Encontrados {len(creditors)} credores (total: {total_count})",
1003
454
  "creditors": creditors,
1004
455
  "count": len(creditors),
1005
- "total_count": total_count,
1006
- "filters_applied": {"limit": limit, "offset": offset, "search": search},
1007
- "request_id": result.get("request_id"),
1008
- "latency_ms": result.get("latency_ms"),
1009
- "cache": result.get("cache")
1010
456
  }
457
+ try:
458
+ _simple_cache_set(cache_key, response, ttl=30)
459
+ except Exception:
460
+ pass
461
+ return response
1011
462
 
1012
463
  return {
1013
464
  "success": False,
1014
465
  "message": "❌ Erro ao buscar credores",
1015
466
  "error": result.get("error"),
1016
467
  "details": result.get("message"),
1017
- "request_id": result.get("request_id")
1018
468
  }
1019
469
 
1020
470
 
1021
471
  @mcp.tool
1022
- async def get_sienge_creditor_bank_info(creditor_id: str,
1023
- _meta: Optional[Dict] = None) -> Dict:
472
+ async def get_sienge_creditor_bank_info(creditor_id: str) -> Dict:
1024
473
  """
1025
474
  Consulta informações bancárias de um credor
1026
475
 
1027
476
  Args:
1028
477
  creditor_id: ID do credor (obrigatório)
1029
478
  """
1030
- # Usar serviço interno
1031
- result = await _svc_get_creditor_bank_info(creditor_id=creditor_id)
479
+ result = await make_sienge_request("GET", f"/creditors/{creditor_id}/bank-informations")
1032
480
 
1033
481
  if result["success"]:
1034
482
  return {
@@ -1036,8 +484,6 @@ async def get_sienge_creditor_bank_info(creditor_id: str,
1036
484
  "message": f"✅ Informações bancárias do credor {creditor_id}",
1037
485
  "creditor_id": creditor_id,
1038
486
  "bank_info": result["data"],
1039
- "request_id": result.get("request_id"),
1040
- "latency_ms": result.get("latency_ms")
1041
487
  }
1042
488
 
1043
489
  return {
@@ -1045,7 +491,6 @@ async def get_sienge_creditor_bank_info(creditor_id: str,
1045
491
  "message": f"❌ Erro ao buscar info bancária do credor {creditor_id}",
1046
492
  "error": result.get("error"),
1047
493
  "details": result.get("message"),
1048
- "request_id": result.get("request_id")
1049
494
  }
1050
495
 
1051
496
 
@@ -1066,10 +511,9 @@ async def get_sienge_accounts_receivable(
1066
511
  origins_ids: Optional[List[str]] = None,
1067
512
  bearers_id_in: Optional[List[int]] = None,
1068
513
  bearers_id_not_in: Optional[List[int]] = None,
1069
- _meta: Optional[Dict] = None) -> Dict:
514
+ ) -> Dict:
1070
515
  """
1071
516
  Consulta parcelas do contas a receber via API bulk-data
1072
- MELHORADO: Suporte a polling assíncrono para requests 202
1073
517
 
1074
518
  Args:
1075
519
  start_date: Data de início do período (YYYY-MM-DD) - OBRIGATÓRIO
@@ -1085,121 +529,90 @@ async def get_sienge_accounts_receivable(
1085
529
  bearers_id_in: Filtrar parcelas com códigos de portador específicos
1086
530
  bearers_id_not_in: Filtrar parcelas excluindo códigos de portador específicos
1087
531
  """
1088
- # Usar serviço interno com polling assíncrono
1089
- result = await _svc_get_accounts_receivable(
1090
- start_date=start_date,
1091
- end_date=end_date,
1092
- selection_type=selection_type,
1093
- company_id=company_id,
1094
- cost_centers_id=cost_centers_id,
1095
- correction_indexer_id=correction_indexer_id,
1096
- correction_date=correction_date,
1097
- change_start_date=change_start_date,
1098
- completed_bills=completed_bills,
1099
- origins_ids=origins_ids,
1100
- bearers_id_in=bearers_id_in,
1101
- bearers_id_not_in=bearers_id_not_in
1102
- )
532
+ params = {"startDate": start_date, "endDate": end_date, "selectionType": selection_type}
533
+
534
+ if company_id:
535
+ params["companyId"] = company_id
536
+ if cost_centers_id:
537
+ params["costCentersId"] = cost_centers_id
538
+ if correction_indexer_id:
539
+ params["correctionIndexerId"] = correction_indexer_id
540
+ if correction_date:
541
+ params["correctionDate"] = correction_date
542
+ if change_start_date:
543
+ params["changeStartDate"] = change_start_date
544
+ if completed_bills:
545
+ params["completedBills"] = completed_bills
546
+ if origins_ids:
547
+ params["originsIds"] = origins_ids
548
+ if bearers_id_in:
549
+ params["bearersIdIn"] = bearers_id_in
550
+ if bearers_id_not_in:
551
+ params["bearersIdNotIn"] = bearers_id_not_in
552
+
553
+ result = await make_sienge_bulk_request("GET", "/income", params=params)
1103
554
 
1104
555
  if result["success"]:
1105
- # Para requests normais (200) e assíncronos processados
1106
- income_data = result.get("data", [])
1107
-
1108
- response = {
556
+ data = result["data"]
557
+ income_data = data.get("data", []) if isinstance(data, dict) else data
558
+
559
+ return {
1109
560
  "success": True,
1110
561
  "message": f"✅ Encontradas {len(income_data)} parcelas a receber",
1111
562
  "income_data": income_data,
1112
563
  "count": len(income_data),
1113
564
  "period": f"{start_date} a {end_date}",
1114
565
  "selection_type": selection_type,
1115
- "request_id": result.get("request_id"),
1116
- "latency_ms": result.get("latency_ms")
566
+ "filters": params,
1117
567
  }
1118
-
1119
- # Se foi processamento assíncrono, incluir informações extras
1120
- if result.get("async_identifier"):
1121
- response.update({
1122
- "async_processing": {
1123
- "identifier": result.get("async_identifier"),
1124
- "correlation_id": result.get("correlation_id"),
1125
- "chunks_downloaded": result.get("chunks_downloaded"),
1126
- "rows_returned": result.get("rows_returned"),
1127
- "polling_attempts": result.get("polling_attempts")
1128
- }
1129
- })
1130
-
1131
- return response
1132
568
 
1133
569
  return {
1134
570
  "success": False,
1135
571
  "message": "❌ Erro ao buscar parcelas a receber",
1136
572
  "error": result.get("error"),
1137
573
  "details": result.get("message"),
1138
- "request_id": result.get("request_id"),
1139
- "async_info": {
1140
- "identifier": result.get("async_identifier"),
1141
- "polling_attempts": result.get("polling_attempts")
1142
- } if result.get("async_identifier") else None
1143
574
  }
1144
575
 
1145
576
 
1146
577
  @mcp.tool
1147
578
  async def get_sienge_accounts_receivable_by_bills(
1148
- bills_ids: List[int], correction_indexer_id: Optional[int] = None, correction_date: Optional[str] = None,
1149
- _meta: Optional[Dict] = None) -> Dict:
579
+ bills_ids: List[int], correction_indexer_id: Optional[int] = None, correction_date: Optional[str] = None
580
+ ) -> Dict:
1150
581
  """
1151
582
  Consulta parcelas dos títulos informados via API bulk-data
1152
- MELHORADO: Suporte a polling assíncrono para requests 202
1153
583
 
1154
584
  Args:
1155
585
  bills_ids: Lista de códigos dos títulos - OBRIGATÓRIO
1156
586
  correction_indexer_id: Código do indexador de correção
1157
587
  correction_date: Data para correção do indexador (YYYY-MM-DD)
1158
588
  """
1159
- # Usar serviço interno com polling assíncrono
1160
- result = await _svc_get_accounts_receivable_by_bills(
1161
- bills_ids=bills_ids,
1162
- correction_indexer_id=correction_indexer_id,
1163
- correction_date=correction_date
1164
- )
589
+ params = {"billsIds": bills_ids}
590
+
591
+ if correction_indexer_id:
592
+ params["correctionIndexerId"] = correction_indexer_id
593
+ if correction_date:
594
+ params["correctionDate"] = correction_date
595
+
596
+ result = await make_sienge_bulk_request("GET", "/income/by-bills", params=params)
1165
597
 
1166
598
  if result["success"]:
1167
- income_data = result.get("data", [])
1168
-
1169
- response = {
599
+ data = result["data"]
600
+ income_data = data.get("data", []) if isinstance(data, dict) else data
601
+
602
+ return {
1170
603
  "success": True,
1171
604
  "message": f"✅ Encontradas {len(income_data)} parcelas dos títulos informados",
1172
605
  "income_data": income_data,
1173
606
  "count": len(income_data),
1174
607
  "bills_consulted": bills_ids,
1175
- "request_id": result.get("request_id"),
1176
- "latency_ms": result.get("latency_ms")
608
+ "filters": params,
1177
609
  }
1178
-
1179
- # Se foi processamento assíncrono, incluir informações extras
1180
- if result.get("async_identifier"):
1181
- response.update({
1182
- "async_processing": {
1183
- "identifier": result.get("async_identifier"),
1184
- "correlation_id": result.get("correlation_id"),
1185
- "chunks_downloaded": result.get("chunks_downloaded"),
1186
- "rows_returned": result.get("rows_returned"),
1187
- "polling_attempts": result.get("polling_attempts")
1188
- }
1189
- })
1190
-
1191
- return response
1192
610
 
1193
611
  return {
1194
612
  "success": False,
1195
613
  "message": "❌ Erro ao buscar parcelas dos títulos informados",
1196
614
  "error": result.get("error"),
1197
615
  "details": result.get("message"),
1198
- "request_id": result.get("request_id"),
1199
- "async_info": {
1200
- "identifier": result.get("async_identifier"),
1201
- "polling_attempts": result.get("polling_attempts")
1202
- } if result.get("async_identifier") else None
1203
616
  }
1204
617
 
1205
618
 
@@ -1210,7 +623,7 @@ async def get_sienge_bills(
1210
623
  creditor_id: Optional[str] = None,
1211
624
  status: Optional[str] = None,
1212
625
  limit: Optional[int] = 50,
1213
- _meta: Optional[Dict] = None) -> Dict:
626
+ ) -> Dict:
1214
627
  """
1215
628
  Consulta títulos a pagar (contas a pagar) - REQUER startDate obrigatório
1216
629
 
@@ -1221,14 +634,26 @@ async def get_sienge_bills(
1221
634
  status: Status do título (ex: open, paid, cancelled)
1222
635
  limit: Máximo de registros (padrão: 50, máx: 200)
1223
636
  """
1224
- # Usar serviço interno
1225
- result = await _svc_get_bills(
1226
- start_date=start_date,
1227
- end_date=end_date,
1228
- creditor_id=creditor_id,
1229
- status=status,
1230
- limit=limit or 50
1231
- )
637
+ from datetime import datetime, timedelta
638
+
639
+ # Se start_date não fornecido, usar últimos 30 dias
640
+ if not start_date:
641
+ start_date = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
642
+
643
+ # Se end_date não fornecido, usar hoje
644
+ if not end_date:
645
+ end_date = datetime.now().strftime("%Y-%m-%d")
646
+
647
+ # Parâmetros obrigatórios
648
+ params = {"startDate": start_date, "endDate": end_date, "limit": min(limit or 50, 200)} # OBRIGATÓRIO pela API
649
+
650
+ # Parâmetros opcionais
651
+ if creditor_id:
652
+ params["creditor_id"] = creditor_id
653
+ if status:
654
+ params["status"] = status
655
+
656
+ result = await make_sienge_request("GET", "/bills", params=params)
1232
657
 
1233
658
  if result["success"]:
1234
659
  data = result["data"]
@@ -1236,33 +661,14 @@ async def get_sienge_bills(
1236
661
  metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
1237
662
  total_count = metadata.get("count", len(bills))
1238
663
 
1239
- # Aplicar parsing numérico nos valores
1240
- for bill in bills:
1241
- if "amount" in bill:
1242
- bill["amount"] = _parse_numeric_value(bill["amount"])
1243
- if "paid_amount" in bill:
1244
- bill["paid_amount"] = _parse_numeric_value(bill["paid_amount"])
1245
- if "remaining_amount" in bill:
1246
- bill["remaining_amount"] = _parse_numeric_value(bill["remaining_amount"])
1247
-
1248
- # Usar datas padrão se não fornecidas
1249
- actual_start = start_date or (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
1250
- actual_end = end_date or datetime.now().strftime("%Y-%m-%d")
1251
-
1252
664
  return {
1253
665
  "success": True,
1254
- "message": f"✅ Encontrados {len(bills)} títulos a pagar (total: {total_count}) - período: {actual_start} a {actual_end}",
666
+ "message": f"✅ Encontrados {len(bills)} títulos a pagar (total: {total_count}) - período: {start_date} a {end_date}",
1255
667
  "bills": bills,
1256
668
  "count": len(bills),
1257
669
  "total_count": total_count,
1258
- "period": {"start_date": actual_start, "end_date": actual_end},
1259
- "filters_applied": {
1260
- "start_date": actual_start, "end_date": actual_end,
1261
- "creditor_id": creditor_id, "status": status, "limit": limit
1262
- },
1263
- "request_id": result.get("request_id"),
1264
- "latency_ms": result.get("latency_ms"),
1265
- "cache": result.get("cache")
670
+ "period": {"start_date": start_date, "end_date": end_date},
671
+ "filters": params,
1266
672
  }
1267
673
 
1268
674
  return {
@@ -1270,7 +676,6 @@ async def get_sienge_bills(
1270
676
  "message": "❌ Erro ao buscar títulos a pagar",
1271
677
  "error": result.get("error"),
1272
678
  "details": result.get("message"),
1273
- "request_id": result.get("request_id")
1274
679
  }
1275
680
 
1276
681
 
@@ -1283,7 +688,7 @@ async def get_sienge_purchase_orders(
1283
688
  status: Optional[str] = None,
1284
689
  date_from: Optional[str] = None,
1285
690
  limit: Optional[int] = 50,
1286
- _meta: Optional[Dict] = None) -> Dict:
691
+ ) -> Dict:
1287
692
  """
1288
693
  Consulta pedidos de compra
1289
694
 
@@ -1316,8 +721,6 @@ async def get_sienge_purchase_orders(
1316
721
  "message": f"✅ Encontrados {len(orders)} pedidos de compra",
1317
722
  "purchase_orders": orders,
1318
723
  "count": len(orders),
1319
- "request_id": result.get("request_id"),
1320
- "latency_ms": result.get("latency_ms")
1321
724
  }
1322
725
 
1323
726
  return {
@@ -1325,14 +728,11 @@ async def get_sienge_purchase_orders(
1325
728
  "message": "❌ Erro ao buscar pedidos de compra",
1326
729
  "error": result.get("error"),
1327
730
  "details": result.get("message"),
1328
- "request_id": result.get("request_id"),
1329
- "latency_ms": result.get("latency_ms")
1330
731
  }
1331
732
 
1332
733
 
1333
734
  @mcp.tool
1334
- async def get_sienge_purchase_order_items(purchase_order_id: str,
1335
- _meta: Optional[Dict] = None) -> Dict:
735
+ async def get_sienge_purchase_order_items(purchase_order_id: str) -> Dict:
1336
736
  """
1337
737
  Consulta itens de um pedido de compra específico
1338
738
 
@@ -1362,8 +762,7 @@ async def get_sienge_purchase_order_items(purchase_order_id: str,
1362
762
 
1363
763
 
1364
764
  @mcp.tool
1365
- async def get_sienge_purchase_requests(purchase_request_id: Optional[str] = None, limit: Optional[int] = 50,
1366
- _meta: Optional[Dict] = None) -> Dict:
765
+ async def get_sienge_purchase_requests(purchase_request_id: Optional[str] = None, limit: Optional[int] = 50) -> Dict:
1367
766
  """
1368
767
  Consulta solicitações de compra
1369
768
 
@@ -1404,8 +803,7 @@ async def get_sienge_purchase_requests(purchase_request_id: Optional[str] = None
1404
803
 
1405
804
 
1406
805
  @mcp.tool
1407
- async def create_sienge_purchase_request(description: str, project_id: str, items: List[Dict[str, Any]],
1408
- _meta: Optional[Dict] = None) -> Dict:
806
+ async def create_sienge_purchase_request(description: str, project_id: str, items: List[Dict[str, Any]]) -> Dict:
1409
807
  """
1410
808
  Cria nova solicitação de compra
1411
809
 
@@ -1421,18 +819,14 @@ async def create_sienge_purchase_request(description: str, project_id: str, item
1421
819
  "date": datetime.now().strftime("%Y-%m-%d"),
1422
820
  }
1423
821
 
1424
- # CORRIGIDO: Normalizar JSON payload
1425
- json_data = to_camel_json(request_data)
1426
- result = await make_sienge_request("POST", "/purchase-requests", json_data=json_data)
822
+ result = await make_sienge_request("POST", "/purchase-requests", json_data=request_data)
1427
823
 
1428
824
  if result["success"]:
1429
825
  return {
1430
826
  "success": True,
1431
827
  "message": "✅ Solicitação de compra criada com sucesso",
1432
- "request_id": result.get("request_id"),
1433
- "purchase_request_id": result["data"].get("id"),
828
+ "request_id": result["data"].get("id"),
1434
829
  "data": result["data"],
1435
- "latency_ms": result.get("latency_ms")
1436
830
  }
1437
831
 
1438
832
  return {
@@ -1440,7 +834,6 @@ async def create_sienge_purchase_request(description: str, project_id: str, item
1440
834
  "message": "❌ Erro ao criar solicitação de compra",
1441
835
  "error": result.get("error"),
1442
836
  "details": result.get("message"),
1443
- "request_id": result.get("request_id")
1444
837
  }
1445
838
 
1446
839
 
@@ -1448,8 +841,7 @@ async def create_sienge_purchase_request(description: str, project_id: str, item
1448
841
 
1449
842
 
1450
843
  @mcp.tool
1451
- async def get_sienge_purchase_invoice(sequential_number: int,
1452
- _meta: Optional[Dict] = None) -> Dict:
844
+ async def get_sienge_purchase_invoice(sequential_number: int) -> Dict:
1453
845
  """
1454
846
  Consulta nota fiscal de compra por número sequencial
1455
847
 
@@ -1470,8 +862,7 @@ async def get_sienge_purchase_invoice(sequential_number: int,
1470
862
 
1471
863
 
1472
864
  @mcp.tool
1473
- async def get_sienge_purchase_invoice_items(sequential_number: int,
1474
- _meta: Optional[Dict] = None) -> Dict:
865
+ async def get_sienge_purchase_invoice_items(sequential_number: int) -> Dict:
1475
866
  """
1476
867
  Consulta itens de uma nota fiscal de compra
1477
868
 
@@ -1512,7 +903,7 @@ async def create_sienge_purchase_invoice(
1512
903
  issue_date: str,
1513
904
  series: Optional[str] = None,
1514
905
  notes: Optional[str] = None,
1515
- _meta: Optional[Dict] = None) -> Dict:
906
+ ) -> Dict:
1516
907
  """
1517
908
  Cadastra uma nova nota fiscal de compra
1518
909
 
@@ -1528,13 +919,13 @@ async def create_sienge_purchase_invoice(
1528
919
  notes: Observações (opcional)
1529
920
  """
1530
921
  invoice_data = {
1531
- "document_id": document_id,
922
+ "documentId": document_id,
1532
923
  "number": number,
1533
- "supplier_id": supplier_id,
1534
- "company_id": company_id,
1535
- "movement_type_id": movement_type_id,
1536
- "movement_date": movement_date,
1537
- "issue_date": issue_date,
924
+ "supplierId": supplier_id,
925
+ "companyId": company_id,
926
+ "movementTypeId": movement_type_id,
927
+ "movementDate": movement_date,
928
+ "issueDate": issue_date,
1538
929
  }
1539
930
 
1540
931
  if series:
@@ -1542,7 +933,7 @@ async def create_sienge_purchase_invoice(
1542
933
  if notes:
1543
934
  invoice_data["notes"] = notes
1544
935
 
1545
- result = await make_sienge_request("POST", "/purchase-invoices", json_data=to_camel_json(invoice_data))
936
+ result = await make_sienge_request("POST", "/purchase-invoices", json_data=invoice_data)
1546
937
 
1547
938
  if result["success"]:
1548
939
  return {"success": True, "message": f"✅ Nota fiscal {number} criada com sucesso", "invoice": result["data"]}
@@ -1562,7 +953,7 @@ async def add_items_to_purchase_invoice(
1562
953
  copy_notes_purchase_orders: bool = True,
1563
954
  copy_notes_resources: bool = False,
1564
955
  copy_attachments_purchase_orders: bool = True,
1565
- _meta: Optional[Dict] = None) -> Dict:
956
+ ) -> Dict:
1566
957
  """
1567
958
  Insere itens em uma nota fiscal a partir de entregas de pedidos de compra
1568
959
 
@@ -1579,14 +970,14 @@ async def add_items_to_purchase_invoice(
1579
970
  copy_attachments_purchase_orders: Copiar anexos dos pedidos de compra
1580
971
  """
1581
972
  item_data = {
1582
- "deliveries_order": deliveries_order,
1583
- "copy_notes_purchase_orders": copy_notes_purchase_orders,
1584
- "copy_notes_resources": copy_notes_resources,
1585
- "copy_attachments_purchase_orders": copy_attachments_purchase_orders,
973
+ "deliveriesOrder": deliveries_order,
974
+ "copyNotesPurchaseOrders": copy_notes_purchase_orders,
975
+ "copyNotesResources": copy_notes_resources,
976
+ "copyAttachmentsPurchaseOrders": copy_attachments_purchase_orders,
1586
977
  }
1587
978
 
1588
979
  result = await make_sienge_request(
1589
- "POST", f"/purchase-invoices/{sequential_number}/items/purchase-orders/delivery-schedules", json_data=to_camel_json(item_data)
980
+ "POST", f"/purchase-invoices/{sequential_number}/items/purchase-orders/delivery-schedules", json_data=item_data
1590
981
  )
1591
982
 
1592
983
  if result["success"]:
@@ -1613,7 +1004,7 @@ async def get_sienge_purchase_invoices_deliveries_attended(
1613
1004
  purchase_order_item_number: Optional[int] = None,
1614
1005
  limit: Optional[int] = 100,
1615
1006
  offset: Optional[int] = 0,
1616
- _meta: Optional[Dict] = None) -> Dict:
1007
+ ) -> Dict:
1617
1008
  """
1618
1009
  Lista entregas atendidas entre pedidos de compra e notas fiscais
1619
1010
 
@@ -1667,8 +1058,7 @@ async def get_sienge_purchase_invoices_deliveries_attended(
1667
1058
 
1668
1059
 
1669
1060
  @mcp.tool
1670
- async def get_sienge_stock_inventory(cost_center_id: str, resource_id: Optional[str] = None,
1671
- _meta: Optional[Dict] = None) -> Dict:
1061
+ async def get_sienge_stock_inventory(cost_center_id: str, resource_id: Optional[str] = None) -> Dict:
1672
1062
  """
1673
1063
  Consulta inventário de estoque por centro de custo
1674
1064
 
@@ -1705,8 +1095,7 @@ async def get_sienge_stock_inventory(cost_center_id: str, resource_id: Optional[
1705
1095
 
1706
1096
 
1707
1097
  @mcp.tool
1708
- async def get_sienge_stock_reservations(limit: Optional[int] = 50,
1709
- _meta: Optional[Dict] = None) -> Dict:
1098
+ async def get_sienge_stock_reservations(limit: Optional[int] = 50) -> Dict:
1710
1099
  """
1711
1100
  Lista reservas de estoque
1712
1101
 
@@ -1746,10 +1135,9 @@ async def get_sienge_projects(
1746
1135
  enterprise_type: Optional[int] = None,
1747
1136
  receivable_register: Optional[str] = None,
1748
1137
  only_buildings_enabled: Optional[bool] = False,
1749
- _meta: Optional[Dict] = None) -> Dict:
1138
+ ) -> Dict:
1750
1139
  """
1751
1140
  Busca empreendimentos/obras no Sienge
1752
- CORRIGIDO: Mapeamento correto da chave de resposta
1753
1141
 
1754
1142
  Args:
1755
1143
  limit: Máximo de registros (padrão: 100, máximo: 200)
@@ -1759,39 +1147,31 @@ async def get_sienge_projects(
1759
1147
  receivable_register: Filtro de registro de recebíveis (B3, CERC)
1760
1148
  only_buildings_enabled: Retornar apenas obras habilitadas para integração orçamentária
1761
1149
  """
1762
- # Usar serviço interno
1763
- result = await _svc_get_projects(
1764
- limit=limit or 100,
1765
- offset=offset or 0,
1766
- company_id=company_id,
1767
- enterprise_type=enterprise_type,
1768
- receivable_register=receivable_register,
1769
- only_buildings_enabled=only_buildings_enabled or False
1770
- )
1150
+ params = {"limit": min(limit or 100, 200), "offset": offset or 0}
1151
+
1152
+ if company_id:
1153
+ params["companyId"] = company_id
1154
+ if enterprise_type:
1155
+ params["type"] = enterprise_type
1156
+ if receivable_register:
1157
+ params["receivableRegister"] = receivable_register
1158
+ if only_buildings_enabled:
1159
+ params["onlyBuildingsEnabledForIntegration"] = only_buildings_enabled
1160
+
1161
+ result = await make_sienge_request("GET", "/enterprises", params=params)
1771
1162
 
1772
1163
  if result["success"]:
1773
1164
  data = result["data"]
1774
- # CORREÇÃO: API retorna em "results", mas paginador espera em "enterprises"
1775
1165
  enterprises = data.get("results", []) if isinstance(data, dict) else data
1776
1166
  metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
1777
- total_count = metadata.get("count", len(enterprises))
1778
1167
 
1779
1168
  return {
1780
1169
  "success": True,
1781
- "message": f"✅ Encontrados {len(enterprises)} empreendimentos (total: {total_count})",
1782
- "enterprises": enterprises, # Manter consistência para paginador
1783
- "projects": enterprises, # Alias para compatibilidade
1170
+ "message": f"✅ Encontrados {len(enterprises)} empreendimentos",
1171
+ "enterprises": enterprises,
1784
1172
  "count": len(enterprises),
1785
- "total_count": total_count,
1786
1173
  "metadata": metadata,
1787
- "filters_applied": {
1788
- "limit": limit, "offset": offset, "company_id": company_id,
1789
- "enterprise_type": enterprise_type, "receivable_register": receivable_register,
1790
- "only_buildings_enabled": only_buildings_enabled
1791
- },
1792
- "request_id": result.get("request_id"),
1793
- "latency_ms": result.get("latency_ms"),
1794
- "cache": result.get("cache")
1174
+ "filters": params,
1795
1175
  }
1796
1176
 
1797
1177
  return {
@@ -1799,13 +1179,11 @@ async def get_sienge_projects(
1799
1179
  "message": "❌ Erro ao buscar empreendimentos",
1800
1180
  "error": result.get("error"),
1801
1181
  "details": result.get("message"),
1802
- "request_id": result.get("request_id")
1803
1182
  }
1804
1183
 
1805
1184
 
1806
1185
  @mcp.tool
1807
- async def get_sienge_enterprise_by_id(enterprise_id: int,
1808
- _meta: Optional[Dict] = None) -> Dict:
1186
+ async def get_sienge_enterprise_by_id(enterprise_id: int) -> Dict:
1809
1187
  """
1810
1188
  Busca um empreendimento específico por ID no Sienge
1811
1189
 
@@ -1826,8 +1204,7 @@ async def get_sienge_enterprise_by_id(enterprise_id: int,
1826
1204
 
1827
1205
 
1828
1206
  @mcp.tool
1829
- async def get_sienge_enterprise_groupings(enterprise_id: int,
1830
- _meta: Optional[Dict] = None) -> Dict:
1207
+ async def get_sienge_enterprise_groupings(enterprise_id: int) -> Dict:
1831
1208
  """
1832
1209
  Busca agrupamentos de unidades de um empreendimento específico
1833
1210
 
@@ -1854,8 +1231,7 @@ async def get_sienge_enterprise_groupings(enterprise_id: int,
1854
1231
 
1855
1232
 
1856
1233
  @mcp.tool
1857
- async def get_sienge_units(limit: Optional[int] = 50, offset: Optional[int] = 0,
1858
- _meta: Optional[Dict] = None) -> Dict:
1234
+ async def get_sienge_units(limit: Optional[int] = 50, offset: Optional[int] = 0) -> Dict:
1859
1235
  """
1860
1236
  Consulta unidades cadastradas no Sienge
1861
1237
 
@@ -1877,9 +1253,6 @@ async def get_sienge_units(limit: Optional[int] = 50, offset: Optional[int] = 0,
1877
1253
  "message": f"✅ Encontradas {len(units)} unidades (total: {total_count})",
1878
1254
  "units": units,
1879
1255
  "count": len(units),
1880
- "total_count": total_count,
1881
- "request_id": result.get("request_id"),
1882
- "latency_ms": result.get("latency_ms")
1883
1256
  }
1884
1257
 
1885
1258
  return {
@@ -1887,8 +1260,6 @@ async def get_sienge_units(limit: Optional[int] = 50, offset: Optional[int] = 0,
1887
1260
  "message": "❌ Erro ao buscar unidades",
1888
1261
  "error": result.get("error"),
1889
1262
  "details": result.get("message"),
1890
- "request_id": result.get("request_id"),
1891
- "latency_ms": result.get("latency_ms")
1892
1263
  }
1893
1264
 
1894
1265
 
@@ -1901,7 +1272,7 @@ async def get_sienge_unit_cost_tables(
1901
1272
  description: Optional[str] = None,
1902
1273
  status: Optional[str] = "Active",
1903
1274
  integration_enabled: Optional[bool] = None,
1904
- _meta: Optional[Dict] = None) -> Dict:
1275
+ ) -> Dict:
1905
1276
  """
1906
1277
  Consulta tabelas de custos unitários
1907
1278
 
@@ -1949,8 +1320,8 @@ async def search_sienge_data(
1949
1320
  query: str,
1950
1321
  entity_type: Optional[str] = None,
1951
1322
  limit: Optional[int] = 20,
1952
- filters: Optional[Dict[str, Any]] = None,
1953
- _meta: Optional[Dict] = None) -> Dict:
1323
+ filters: Optional[Dict[str, Any]] = None
1324
+ ) -> Dict:
1954
1325
  """
1955
1326
  Busca universal no Sienge - compatível com ChatGPT/OpenAI MCP
1956
1327
 
@@ -2027,44 +1398,35 @@ async def search_sienge_data(
2027
1398
 
2028
1399
 
2029
1400
  async def _search_specific_entity(entity_type: str, query: str, limit: int, filters: Dict) -> Dict:
2030
- """
2031
- Função auxiliar para buscar em uma entidade específica
2032
- CORRIGIDO: Usa serviços internos, nunca outras tools
2033
- """
1401
+ """Função auxiliar para buscar em uma entidade específica"""
2034
1402
 
2035
1403
  if entity_type == "customers":
2036
- result = await _svc_get_customers(limit=limit, search=query)
1404
+ result = await get_sienge_customers(limit=limit, search=query)
2037
1405
  if result["success"]:
2038
- data = result["data"]
2039
- customers = data.get("results", []) if isinstance(data, dict) else data
2040
1406
  return {
2041
1407
  "success": True,
2042
- "data": customers,
2043
- "count": len(customers),
1408
+ "data": result["customers"],
1409
+ "count": result["count"],
2044
1410
  "entity_type": "customers"
2045
1411
  }
2046
1412
 
2047
1413
  elif entity_type == "creditors":
2048
- result = await _svc_get_creditors(limit=limit, search=query)
1414
+ result = await get_sienge_creditors(limit=limit, search=query)
2049
1415
  if result["success"]:
2050
- data = result["data"]
2051
- creditors = data.get("results", []) if isinstance(data, dict) else data
2052
1416
  return {
2053
1417
  "success": True,
2054
- "data": creditors,
2055
- "count": len(creditors),
1418
+ "data": result["creditors"],
1419
+ "count": result["count"],
2056
1420
  "entity_type": "creditors"
2057
1421
  }
2058
1422
 
2059
1423
  elif entity_type == "projects" or entity_type == "enterprises":
2060
1424
  # Para projetos, usar filtros mais específicos se disponível
2061
1425
  company_id = filters.get("company_id")
2062
- result = await _svc_get_projects(limit=limit, company_id=company_id)
1426
+ result = await get_sienge_projects(limit=limit, company_id=company_id)
2063
1427
  if result["success"]:
2064
- data = result["data"]
2065
- projects = data.get("results", []) if isinstance(data, dict) else data
2066
-
2067
1428
  # Filtrar por query se fornecida
1429
+ projects = result["enterprises"]
2068
1430
  if query:
2069
1431
  projects = [
2070
1432
  p for p in projects
@@ -2083,27 +1445,23 @@ async def _search_specific_entity(entity_type: str, query: str, limit: int, filt
2083
1445
  # Para títulos, usar data padrão se não especificada
2084
1446
  start_date = filters.get("start_date")
2085
1447
  end_date = filters.get("end_date")
2086
- result = await _svc_get_bills(
1448
+ result = await get_sienge_bills(
2087
1449
  start_date=start_date,
2088
1450
  end_date=end_date,
2089
1451
  limit=limit
2090
1452
  )
2091
1453
  if result["success"]:
2092
- data = result["data"]
2093
- bills = data.get("results", []) if isinstance(data, dict) else data
2094
1454
  return {
2095
1455
  "success": True,
2096
- "data": bills,
2097
- "count": len(bills),
1456
+ "data": result["bills"],
1457
+ "count": result["count"],
2098
1458
  "entity_type": "bills"
2099
1459
  }
2100
1460
 
2101
1461
  elif entity_type == "purchase_orders":
2102
- result = await _svc_get_purchase_orders(limit=limit)
1462
+ result = await get_sienge_purchase_orders(limit=limit)
2103
1463
  if result["success"]:
2104
- data = result["data"]
2105
- orders = data.get("results", []) if isinstance(data, dict) else data
2106
-
1464
+ orders = result["purchase_orders"]
2107
1465
  # Filtrar por query se fornecida
2108
1466
  if query:
2109
1467
  orders = [
@@ -2127,7 +1485,7 @@ async def _search_specific_entity(entity_type: str, query: str, limit: int, filt
2127
1485
 
2128
1486
 
2129
1487
  @mcp.tool
2130
- async def list_sienge_entities(_meta: Optional[Dict] = None) -> Dict:
1488
+ async def list_sienge_entities() -> Dict:
2131
1489
  """
2132
1490
  Lista todas as entidades disponíveis no Sienge MCP para busca
2133
1491
 
@@ -2146,28 +1504,28 @@ async def list_sienge_entities(_meta: Optional[Dict] = None) -> Dict:
2146
1504
  "name": "Credores/Fornecedores",
2147
1505
  "description": "Fornecedores e credores cadastrados",
2148
1506
  "search_fields": ["nome", "documento"],
2149
- "tools": ["get_sienge_creditors", "get_sienge_creditor_bank_info", "get_sienge_suppliers"]
1507
+ "tools": ["get_sienge_creditors", "get_sienge_creditor_bank_info"]
2150
1508
  },
2151
1509
  {
2152
1510
  "type": "projects",
2153
1511
  "name": "Empreendimentos/Obras",
2154
1512
  "description": "Projetos e obras cadastrados",
2155
1513
  "search_fields": ["código", "descrição", "nome"],
2156
- "tools": ["get_sienge_projects", "get_sienge_enterprises", "get_sienge_enterprise_by_id"]
1514
+ "tools": ["get_sienge_projects", "get_sienge_enterprise_by_id"]
2157
1515
  },
2158
1516
  {
2159
1517
  "type": "bills",
2160
1518
  "name": "Títulos a Pagar",
2161
1519
  "description": "Contas a pagar e títulos financeiros",
2162
1520
  "search_fields": ["número", "credor", "valor"],
2163
- "tools": ["get_sienge_bills", "get_sienge_accounts_payable"]
1521
+ "tools": ["get_sienge_bills"]
2164
1522
  },
2165
1523
  {
2166
1524
  "type": "purchase_orders",
2167
1525
  "name": "Pedidos de Compra",
2168
1526
  "description": "Pedidos de compra e solicitações",
2169
1527
  "search_fields": ["id", "descrição", "status"],
2170
- "tools": ["get_sienge_purchase_orders", "get_sienge_purchase_requests", "list_sienge_purchase_requests"]
1528
+ "tools": ["get_sienge_purchase_orders", "get_sienge_purchase_requests"]
2171
1529
  },
2172
1530
  {
2173
1531
  "type": "invoices",
@@ -2188,14 +1546,7 @@ async def list_sienge_entities(_meta: Optional[Dict] = None) -> Dict:
2188
1546
  "name": "Financeiro",
2189
1547
  "description": "Contas a receber e movimentações financeiras",
2190
1548
  "search_fields": ["período", "cliente", "valor"],
2191
- "tools": ["get_sienge_accounts_receivable", "search_sienge_financial_data", "search_sienge_finances"]
2192
- },
2193
- {
2194
- "type": "suppliers",
2195
- "name": "Fornecedores",
2196
- "description": "Fornecedores e credores cadastrados",
2197
- "search_fields": ["código", "nome", "razão social"],
2198
- "tools": ["get_sienge_suppliers"]
1549
+ "tools": ["get_sienge_accounts_receivable"]
2199
1550
  }
2200
1551
  ]
2201
1552
 
@@ -2215,93 +1566,54 @@ async def list_sienge_entities(_meta: Optional[Dict] = None) -> Dict:
2215
1566
  # ============ PAGINATION E NAVEGAÇÃO ============
2216
1567
 
2217
1568
 
2218
- @mcp.tool
2219
- async def get_sienge_data_paginated(
1569
+ async def _get_data_paginated_internal(
2220
1570
  entity_type: str,
2221
1571
  page: int = 1,
2222
1572
  page_size: int = 20,
2223
1573
  filters: Optional[Dict[str, Any]] = None,
2224
- sort_by: Optional[str] = None,
2225
- _meta: Optional[Dict] = None) -> Dict:
2226
- """
2227
- Busca dados do Sienge com paginação avançada - compatível com ChatGPT
2228
-
2229
- Args:
2230
- entity_type: Tipo de entidade (customers, creditors, projects, bills, etc.)
2231
- page: Número da página (começando em 1)
2232
- page_size: Registros por página (máximo 50)
2233
- filters: Filtros específicos da entidade
2234
- sort_by: Campo para ordenação (se suportado)
2235
- """
1574
+ sort_by: Optional[str] = None
1575
+ ) -> Dict:
1576
+ """Função interna para paginação (sem decorador @mcp.tool)"""
2236
1577
  page_size = min(page_size, 50)
2237
1578
  offset = (page - 1) * page_size
2238
1579
 
2239
1580
  filters = filters or {}
2240
1581
 
2241
- # CORRIGIDO: Mapear para serviços internos, não tools
1582
+ # Mapear para os tools existentes com offset
2242
1583
  if entity_type == "customers":
2243
1584
  search = filters.get("search")
2244
1585
  customer_type_id = filters.get("customer_type_id")
2245
- result = await _svc_get_customers(
1586
+ result = await get_sienge_customers(
2246
1587
  limit=page_size,
2247
1588
  offset=offset,
2248
1589
  search=search,
2249
1590
  customer_type_id=customer_type_id
2250
1591
  )
2251
- # CORRIGIDO: Extrair items e total corretamente
2252
- if result["success"]:
2253
- data = result["data"]
2254
- items, total = _extract_items_and_total(data)
2255
- result["customers"] = items
2256
- result["count"] = len(items)
2257
- result["total_count"] = total
2258
1592
 
2259
1593
  elif entity_type == "creditors":
2260
1594
  search = filters.get("search")
2261
- result = await _svc_get_creditors(
1595
+ result = await get_sienge_creditors(
2262
1596
  limit=page_size,
2263
1597
  offset=offset,
2264
1598
  search=search
2265
1599
  )
2266
- # CORRIGIDO: Extrair items e total corretamente
2267
- if result["success"]:
2268
- data = result["data"]
2269
- items, total = _extract_items_and_total(data)
2270
- result["creditors"] = items
2271
- result["count"] = len(items)
2272
- result["total_count"] = total
2273
1600
 
2274
1601
  elif entity_type == "projects":
2275
- result = await _svc_get_projects(
1602
+ result = await get_sienge_projects(
2276
1603
  limit=page_size,
2277
1604
  offset=offset,
2278
1605
  company_id=filters.get("company_id"),
2279
1606
  enterprise_type=filters.get("enterprise_type")
2280
1607
  )
2281
- # CORRIGIDO: Extrair items e total corretamente
2282
- if result["success"]:
2283
- data = result["data"]
2284
- items, total = _extract_items_and_total(data)
2285
- result["projects"] = items
2286
- result["enterprises"] = items # Para compatibilidade
2287
- result["count"] = len(items)
2288
- result["total_count"] = total
2289
1608
 
2290
1609
  elif entity_type == "bills":
2291
- result = await _svc_get_bills(
1610
+ result = await get_sienge_bills(
2292
1611
  start_date=filters.get("start_date"),
2293
1612
  end_date=filters.get("end_date"),
2294
1613
  creditor_id=filters.get("creditor_id"),
2295
1614
  status=filters.get("status"),
2296
1615
  limit=page_size
2297
1616
  )
2298
- # CORRIGIDO: Extrair items e total corretamente
2299
- if result["success"]:
2300
- data = result["data"]
2301
- items, total = _extract_items_and_total(data)
2302
- result["bills"] = items
2303
- result["count"] = len(items)
2304
- result["total_count"] = total
2305
1617
 
2306
1618
  else:
2307
1619
  return {
@@ -2336,26 +1648,42 @@ async def get_sienge_data_paginated(
2336
1648
  return result
2337
1649
 
2338
1650
 
2339
- @mcp.tool
2340
- async def search_sienge_financial_data(
2341
- period_start: str,
2342
- period_end: str,
2343
- search_type: str = "both",
2344
- amount_min: Optional[float] = None,
2345
- amount_max: Optional[float] = None,
2346
- customer_creditor_search: Optional[str] = None,
2347
- _meta: Optional[Dict] = None) -> Dict:
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:
2348
1659
  """
2349
- Busca avançada em dados financeiros do Sienge - Contas a Pagar e Receber
1660
+ Busca dados do Sienge com paginação avançada - compatível com ChatGPT
2350
1661
 
2351
1662
  Args:
2352
- period_start: Data inicial do período (YYYY-MM-DD)
2353
- period_end: Data final do período (YYYY-MM-DD)
2354
- search_type: Tipo de busca ("receivable", "payable", "both")
2355
- amount_min: Valor mínimo (opcional)
2356
- amount_max: Valor máximo (opcional)
2357
- customer_creditor_search: Buscar por nome de cliente/credor (opcional)
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)
2358
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(
1679
+ period_start: str,
1680
+ period_end: str,
1681
+ search_type: str = "both",
1682
+ amount_min: Optional[float] = None,
1683
+ amount_max: Optional[float] = None,
1684
+ customer_creditor_search: Optional[str] = None
1685
+ ) -> Dict:
1686
+ """Função interna para busca financeira (sem decorador @mcp.tool)"""
2359
1687
 
2360
1688
  financial_results = {
2361
1689
  "receivable": {"success": False, "data": [], "count": 0, "error": None},
@@ -2365,21 +1693,20 @@ async def search_sienge_financial_data(
2365
1693
  # Buscar contas a receber
2366
1694
  if search_type in ["receivable", "both"]:
2367
1695
  try:
2368
- # CORRIGIDO: Usar serviço interno
2369
- receivable_result = await _svc_get_accounts_receivable(
1696
+ receivable_result = await get_sienge_accounts_receivable(
2370
1697
  start_date=period_start,
2371
1698
  end_date=period_end,
2372
1699
  selection_type="D" # Por vencimento
2373
1700
  )
2374
1701
 
2375
1702
  if receivable_result["success"]:
2376
- receivable_data = receivable_result.get("data", [])
1703
+ receivable_data = receivable_result["income_data"]
2377
1704
 
2378
- # CORRIGIDO: Aplicar filtros de valor usando _parse_numeric_value
1705
+ # Aplicar filtros de valor se especificados
2379
1706
  if amount_min is not None or amount_max is not None:
2380
1707
  filtered_data = []
2381
1708
  for item in receivable_data:
2382
- amount = _parse_numeric_value(item.get("amount", 0))
1709
+ amount = float(item.get("amount", 0) or 0)
2383
1710
  if amount_min is not None and amount < amount_min:
2384
1711
  continue
2385
1712
  if amount_max is not None and amount > amount_max:
@@ -2412,22 +1739,20 @@ async def search_sienge_financial_data(
2412
1739
  # Buscar contas a pagar
2413
1740
  if search_type in ["payable", "both"]:
2414
1741
  try:
2415
- # CORRIGIDO: Usar serviço interno
2416
- payable_result = await _svc_get_bills(
1742
+ payable_result = await get_sienge_bills(
2417
1743
  start_date=period_start,
2418
1744
  end_date=period_end,
2419
1745
  limit=100
2420
1746
  )
2421
1747
 
2422
1748
  if payable_result["success"]:
2423
- data = payable_result["data"]
2424
- payable_data = data.get("results", []) if isinstance(data, dict) else data
1749
+ payable_data = payable_result["bills"]
2425
1750
 
2426
- # CORRIGIDO: Aplicar filtros de valor usando _parse_numeric_value
1751
+ # Aplicar filtros de valor se especificados
2427
1752
  if amount_min is not None or amount_max is not None:
2428
1753
  filtered_data = []
2429
1754
  for item in payable_data:
2430
- amount = _parse_numeric_value(item.get("amount", 0))
1755
+ amount = float(item.get("amount", 0) or 0)
2431
1756
  if amount_min is not None and amount < amount_min:
2432
1757
  continue
2433
1758
  if amount_max is not None and amount > amount_max:
@@ -2494,50 +1819,39 @@ async def search_sienge_financial_data(
2494
1819
  }
2495
1820
 
2496
1821
 
2497
- async def _svc_test_connection() -> Dict:
2498
- """Serviço interno: testar conexão com a API do Sienge"""
2499
- try:
2500
- # Usar serviço interno
2501
- result = await _svc_get_customer_types()
2502
-
2503
- if result["success"]:
2504
- auth_method = "Bearer Token" if SIENGE_API_KEY else "Basic Auth"
2505
- return {
2506
- "success": True,
2507
- "message": "✅ Conexão com API do Sienge estabelecida com sucesso!",
2508
- "api_status": "Online",
2509
- "auth_method": auth_method,
2510
- "timestamp": datetime.now().isoformat(),
2511
- "request_id": result.get("request_id"),
2512
- "latency_ms": result.get("latency_ms"),
2513
- "cache": result.get("cache")
2514
- }
2515
- else:
2516
- return {
2517
- "success": False,
2518
- "message": "❌ Falha ao conectar com API do Sienge",
2519
- "error": result.get("error"),
2520
- "details": result.get("message"),
2521
- "timestamp": datetime.now().isoformat(),
2522
- "request_id": result.get("request_id")
2523
- }
2524
- except Exception as e:
2525
- return {
2526
- "success": False,
2527
- "message": "❌ Erro ao testar conexão",
2528
- "error": str(e),
2529
- "timestamp": datetime.now().isoformat(),
2530
- }
2531
-
2532
-
2533
1822
  @mcp.tool
2534
- async def test_sienge_connection(_meta: Optional[Dict] = None) -> Dict:
2535
- """Testa a conexão com a API do Sienge"""
2536
- return await _svc_test_connection()
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:
1831
+ """
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)
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
+ )
2537
1850
 
2538
1851
 
2539
- async def _svc_get_dashboard_summary() -> Dict:
2540
- """Serviço interno: obter resumo do dashboard"""
1852
+ async def _get_dashboard_summary_internal() -> Dict:
1853
+ """Função interna para dashboard (sem decorador @mcp.tool)"""
1854
+
2541
1855
  # Data atual e períodos
2542
1856
  today = datetime.now()
2543
1857
  current_month_start = today.replace(day=1).strftime("%Y-%m-%d")
@@ -2548,7 +1862,7 @@ async def _svc_get_dashboard_summary() -> Dict:
2548
1862
 
2549
1863
  # 1. Testar conexão
2550
1864
  try:
2551
- connection_test = await _svc_test_connection()
1865
+ connection_test = await test_sienge_connection()
2552
1866
  dashboard_data["connection"] = connection_test
2553
1867
  except Exception as e:
2554
1868
  errors.append(f"Teste de conexão: {str(e)}")
@@ -2556,24 +1870,23 @@ async def _svc_get_dashboard_summary() -> Dict:
2556
1870
 
2557
1871
  # 2. Contar clientes (amostra)
2558
1872
  try:
2559
- customers_result = await _svc_get_customers(limit=1)
2560
- dashboard_data["customers_available"] = customers_result["success"]
1873
+ customers_result = await get_sienge_customers(limit=1)
1874
+ if customers_result["success"]:
1875
+ dashboard_data["customers_available"] = True
1876
+ else:
1877
+ dashboard_data["customers_available"] = False
2561
1878
  except Exception as e:
2562
1879
  errors.append(f"Clientes: {str(e)}")
2563
1880
  dashboard_data["customers_available"] = False
2564
1881
 
2565
1882
  # 3. Contar projetos (amostra)
2566
1883
  try:
2567
- projects_result = await _svc_get_projects(limit=5)
1884
+ projects_result = await get_sienge_projects(limit=5)
2568
1885
  if projects_result["success"]:
2569
- data = projects_result["data"]
2570
- enterprises = data.get("results", []) if isinstance(data, dict) else data
2571
- metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
2572
-
2573
1886
  dashboard_data["projects"] = {
2574
1887
  "available": True,
2575
- "sample_count": len(enterprises),
2576
- "total_count": metadata.get("count", "N/A")
1888
+ "sample_count": len(projects_result["enterprises"]),
1889
+ "total_count": projects_result.get("metadata", {}).get("count", "N/A")
2577
1890
  }
2578
1891
  else:
2579
1892
  dashboard_data["projects"] = {"available": False}
@@ -2583,20 +1896,16 @@ async def _svc_get_dashboard_summary() -> Dict:
2583
1896
 
2584
1897
  # 4. Títulos a pagar do mês atual
2585
1898
  try:
2586
- bills_result = await _svc_get_bills(
1899
+ bills_result = await get_sienge_bills(
2587
1900
  start_date=current_month_start,
2588
1901
  end_date=current_month_end,
2589
1902
  limit=10
2590
1903
  )
2591
1904
  if bills_result["success"]:
2592
- data = bills_result["data"]
2593
- bills = data.get("results", []) if isinstance(data, dict) else data
2594
- metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
2595
-
2596
1905
  dashboard_data["monthly_bills"] = {
2597
1906
  "available": True,
2598
- "count": len(bills),
2599
- "total_count": metadata.get("count", len(bills))
1907
+ "count": len(bills_result["bills"]),
1908
+ "total_count": bills_result.get("total_count", len(bills_result["bills"]))
2600
1909
  }
2601
1910
  else:
2602
1911
  dashboard_data["monthly_bills"] = {"available": False}
@@ -2606,14 +1915,11 @@ async def _svc_get_dashboard_summary() -> Dict:
2606
1915
 
2607
1916
  # 5. Tipos de clientes
2608
1917
  try:
2609
- customer_types_result = await _svc_get_customer_types()
1918
+ customer_types_result = await get_sienge_customer_types()
2610
1919
  if customer_types_result["success"]:
2611
- data = customer_types_result["data"]
2612
- customer_types = data.get("results", []) if isinstance(data, dict) else data
2613
-
2614
1920
  dashboard_data["customer_types"] = {
2615
1921
  "available": True,
2616
- "count": len(customer_types)
1922
+ "count": len(customer_types_result["customer_types"])
2617
1923
  }
2618
1924
  else:
2619
1925
  dashboard_data["customer_types"] = {"available": False}
@@ -2643,12 +1949,298 @@ async def _svc_get_dashboard_summary() -> Dict:
2643
1949
 
2644
1950
 
2645
1951
  @mcp.tool
2646
- async def get_sienge_dashboard_summary(_meta: Optional[Dict] = None) -> Dict:
1952
+ async def get_sienge_dashboard_summary() -> Dict:
2647
1953
  """
2648
1954
  Obtém um resumo tipo dashboard com informações gerais do Sienge
2649
1955
  Útil para visão geral rápida do sistema
2650
1956
  """
2651
- return await _svc_get_dashboard_summary()
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
+ }
2652
2244
 
2653
2245
 
2654
2246
  # ============ UTILITÁRIOS ============
@@ -2660,21 +2252,6 @@ def add(a: int, b: int) -> int:
2660
2252
  return a + b
2661
2253
 
2662
2254
 
2663
- def _mask(s: str) -> str:
2664
- """Mascara dados sensíveis mantendo apenas o início e fim"""
2665
- if not s:
2666
- return None
2667
- if len(s) == 1:
2668
- return s + "*"
2669
- if len(s) == 2:
2670
- return s
2671
- if len(s) <= 4:
2672
- return s[:2] + "*" * (len(s) - 2)
2673
- # Para strings > 4: usar no máximo 4 asteriscos no meio
2674
- middle_asterisks = min(4, len(s) - 4)
2675
- return s[:2] + "*" * middle_asterisks + s[-2:]
2676
-
2677
-
2678
2255
  def _get_auth_info_internal() -> Dict:
2679
2256
  """Função interna para verificar configuração de autenticação"""
2680
2257
  if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
@@ -2685,7 +2262,7 @@ def _get_auth_info_internal() -> Dict:
2685
2262
  "configured": True,
2686
2263
  "base_url": SIENGE_BASE_URL,
2687
2264
  "subdomain": SIENGE_SUBDOMAIN,
2688
- "username": _mask(SIENGE_USERNAME) if SIENGE_USERNAME else None,
2265
+ "username": SIENGE_USERNAME,
2689
2266
  }
2690
2267
  else:
2691
2268
  return {
@@ -2695,6 +2272,262 @@ def _get_auth_info_internal() -> Dict:
2695
2272
  }
2696
2273
 
2697
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
+
2448
+ # ============ SIMPLE ASYNC CACHE (in-memory, process-local) ============
2449
+ # Lightweight helper to improve hit-rate on repeated test queries
2450
+ _SIMPLE_CACHE: Dict[str, Dict[str, Any]] = {}
2451
+
2452
+ def _simple_cache_set(key: str, value: Dict[str, Any], ttl: int = 60) -> None:
2453
+ expire_at = int(time.time()) + int(ttl)
2454
+ _SIMPLE_CACHE[key] = {"value": value, "expire_at": expire_at}
2455
+
2456
+ def _simple_cache_get(key: str) -> Optional[Dict[str, Any]]:
2457
+ item = _SIMPLE_CACHE.get(key)
2458
+ if not item:
2459
+ return None
2460
+ if int(time.time()) > item.get("expire_at", 0):
2461
+ try:
2462
+ del _SIMPLE_CACHE[key]
2463
+ except KeyError:
2464
+ pass
2465
+ return None
2466
+ return item.get("value")
2467
+
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
+
2698
2531
  @mcp.tool
2699
2532
  def get_auth_info() -> Dict:
2700
2533
  """Retorna informações sobre a configuração de autenticação"""
@@ -2733,4 +2566,4 @@ def main():
2733
2566
 
2734
2567
 
2735
2568
  if __name__ == "__main__":
2736
- main()
2569
+ main()