sienge-ecbiesek-mcp 1.1.5__py3-none-any.whl → 1.2.1__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,14 +2,27 @@
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
5
12
  """
6
13
 
7
14
  from fastmcp import FastMCP
8
15
  import httpx
9
- from typing import Dict, List, Optional, Any
16
+ from typing import Dict, List, Optional, Any, Union
10
17
  import os
11
18
  from dotenv import load_dotenv
12
- from datetime import datetime
19
+ from datetime import datetime, timedelta
20
+ import uuid
21
+ import asyncio
22
+ import json
23
+ import time
24
+ import logging
25
+ from functools import wraps
13
26
 
14
27
  # Carrega as variáveis de ambiente
15
28
  load_dotenv()
@@ -24,61 +37,273 @@ SIENGE_PASSWORD = os.getenv("SIENGE_PASSWORD", "")
24
37
  SIENGE_API_KEY = os.getenv("SIENGE_API_KEY", "")
25
38
  REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", "30"))
26
39
 
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
+
27
51
 
28
52
  class SiengeAPIError(Exception):
29
53
  """Exceção customizada para erros da API do Sienge"""
30
-
31
54
  pass
32
55
 
33
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
+
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
188
+
189
+
34
190
  async def make_sienge_request(
35
- method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None
191
+ method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None, use_cache: bool = True
36
192
  ) -> Dict:
37
193
  """
38
194
  Função auxiliar para fazer requisições à API do Sienge (v1)
39
195
  Suporta tanto Bearer Token quanto Basic Auth
196
+ MELHORADO: Observabilidade, cache, normalização de parâmetros
40
197
  """
198
+ start_time = time.time()
199
+ req_id = str(uuid.uuid4())
200
+
41
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
+
42
214
  async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
43
- headers = {"Content-Type": "application/json", "Accept": "application/json"}
215
+ headers = {
216
+ "Content-Type": "application/json",
217
+ "Accept": "application/json",
218
+ "X-Request-ID": req_id
219
+ }
44
220
 
45
- # Configurar autenticação e URL
221
+ # Configurar autenticação e URL (corrigindo URLs duplas)
46
222
  auth = None
223
+ base_normalized = _normalize_url(SIENGE_BASE_URL, SIENGE_SUBDOMAIN)
47
224
 
48
225
  if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
49
- # Bearer Token (Recomendado)
50
226
  headers["Authorization"] = f"Bearer {SIENGE_API_KEY}"
51
- url = f"{SIENGE_BASE_URL}/{SIENGE_SUBDOMAIN}/public/api/v1{endpoint}"
227
+ url = f"{base_normalized}/v1{endpoint}"
52
228
  elif SIENGE_USERNAME and SIENGE_PASSWORD:
53
- # Basic Auth usando httpx.BasicAuth
54
229
  auth = httpx.BasicAuth(SIENGE_USERNAME, SIENGE_PASSWORD)
55
- url = f"{SIENGE_BASE_URL}/{SIENGE_SUBDOMAIN}/public/api/v1{endpoint}"
230
+ url = f"{base_normalized}/v1{endpoint}"
56
231
  else:
57
232
  return {
58
233
  "success": False,
59
234
  "error": "No Authentication",
60
235
  "message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env",
236
+ "request_id": req_id
61
237
  }
62
238
 
63
- response = await client.request(method=method, url=url, headers=headers, params=params, json=json_data, auth=auth)
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)
64
250
 
65
251
  if response.status_code in [200, 201]:
66
252
  try:
67
- return {"success": True, "data": response.json(), "status_code": response.status_code}
68
- except BaseException:
69
- return {"success": True, "data": {"message": "Success"}, "status_code": response.status_code}
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:
270
+ return {
271
+ "success": True,
272
+ "data": {"message": "Success"},
273
+ "status_code": response.status_code,
274
+ "request_id": req_id,
275
+ "latency_ms": round(latency * 1000, 2)
276
+ }
70
277
  else:
71
278
  return {
72
279
  "success": False,
73
280
  "error": f"HTTP {response.status_code}",
74
281
  "message": response.text,
75
282
  "status_code": response.status_code,
283
+ "request_id": req_id,
284
+ "latency_ms": round(latency * 1000, 2)
76
285
  }
77
286
 
78
287
  except httpx.TimeoutException:
79
- return {"success": False, "error": "Timeout", "message": f"A requisição excedeu o tempo limite de {REQUEST_TIMEOUT}s"}
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
+ }
80
297
  except Exception as e:
81
- return {"success": False, "error": str(e), "message": f"Erro na requisição: {str(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
+ }
82
307
 
83
308
 
84
309
  async def make_sienge_bulk_request(
@@ -87,59 +312,359 @@ async def make_sienge_bulk_request(
87
312
  """
88
313
  Função auxiliar para fazer requisições à API bulk-data do Sienge
89
314
  Suporta tanto Bearer Token quanto Basic Auth
315
+ MELHORADO: Observabilidade e normalização de parâmetros
90
316
  """
317
+ start_time = time.time()
318
+ req_id = str(uuid.uuid4())
319
+
91
320
  try:
321
+ # Normalizar parâmetros
322
+ normalized_params = to_query(params) if params else None
323
+
92
324
  async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
93
- headers = {"Content-Type": "application/json", "Accept": "application/json"}
325
+ headers = {
326
+ "Content-Type": "application/json",
327
+ "Accept": "application/json",
328
+ "X-Request-ID": req_id
329
+ }
94
330
 
95
- # Configurar autenticação e URL para bulk-data
331
+ # Configurar autenticação e URL para bulk-data (corrigindo URLs duplas)
96
332
  auth = None
333
+ base_normalized = _normalize_url(SIENGE_BASE_URL, SIENGE_SUBDOMAIN)
97
334
 
98
335
  if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
99
- # Bearer Token (Recomendado)
100
336
  headers["Authorization"] = f"Bearer {SIENGE_API_KEY}"
101
- url = f"{SIENGE_BASE_URL}/{SIENGE_SUBDOMAIN}/public/api/bulk-data/v1{endpoint}"
337
+ url = f"{base_normalized}/bulk-data/v1{endpoint}"
102
338
  elif SIENGE_USERNAME and SIENGE_PASSWORD:
103
- # Basic Auth usando httpx.BasicAuth
104
339
  auth = httpx.BasicAuth(SIENGE_USERNAME, SIENGE_PASSWORD)
105
- url = f"{SIENGE_BASE_URL}/{SIENGE_SUBDOMAIN}/public/api/bulk-data/v1{endpoint}"
340
+ url = f"{base_normalized}/bulk-data/v1{endpoint}"
106
341
  else:
107
342
  return {
108
343
  "success": False,
109
344
  "error": "No Authentication",
110
345
  "message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env",
346
+ "request_id": req_id
111
347
  }
112
348
 
113
- response = await client.request(method=method, url=url, headers=headers, params=params, json=json_data, auth=auth)
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)
114
360
 
115
- if response.status_code in [200, 201]:
361
+ if response.status_code in [200, 201, 202]:
116
362
  try:
117
- return {"success": True, "data": response.json(), "status_code": response.status_code}
118
- except BaseException:
119
- return {"success": True, "data": {"message": "Success"}, "status_code": response.status_code}
363
+ return {
364
+ "success": True,
365
+ "data": response.json(),
366
+ "status_code": response.status_code,
367
+ "request_id": req_id,
368
+ "latency_ms": round(latency * 1000, 2)
369
+ }
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
+ }
120
378
  else:
121
379
  return {
122
380
  "success": False,
123
381
  "error": f"HTTP {response.status_code}",
124
382
  "message": response.text,
125
383
  "status_code": response.status_code,
384
+ "request_id": req_id,
385
+ "latency_ms": round(latency * 1000, 2)
126
386
  }
127
387
 
128
388
  except httpx.TimeoutException:
129
- return {"success": False, "error": "Timeout", "message": f"A requisição excedeu o tempo limite de {REQUEST_TIMEOUT}s"}
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
+ }
130
398
  except Exception as e:
131
- return {"success": False, "error": str(e), "message": f"Erro na requisição bulk-data: {str(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)
132
657
 
133
658
 
134
659
  # ============ CONEXÃO E TESTE ============
135
660
 
136
661
 
137
662
  @mcp.tool
138
- async def test_sienge_connection() -> Dict:
663
+ async def test_sienge_connection(_meta: Optional[Dict] = None) -> Dict:
139
664
  """Testa a conexão com a API do Sienge"""
140
665
  try:
141
- # Tentar endpoint mais simples primeiro
142
- result = await make_sienge_request("GET", "/customer-types")
666
+ # Usar serviço interno
667
+ result = await _svc_get_customer_types()
143
668
 
144
669
  if result["success"]:
145
670
  auth_method = "Bearer Token" if SIENGE_API_KEY else "Basic Auth"
@@ -149,6 +674,9 @@ async def test_sienge_connection() -> Dict:
149
674
  "api_status": "Online",
150
675
  "auth_method": auth_method,
151
676
  "timestamp": datetime.now().isoformat(),
677
+ "request_id": result.get("request_id"),
678
+ "latency_ms": result.get("latency_ms"),
679
+ "cache": result.get("cache")
152
680
  }
153
681
  else:
154
682
  return {
@@ -157,6 +685,7 @@ async def test_sienge_connection() -> Dict:
157
685
  "error": result.get("error"),
158
686
  "details": result.get("message"),
159
687
  "timestamp": datetime.now().isoformat(),
688
+ "request_id": result.get("request_id")
160
689
  }
161
690
  except Exception as e:
162
691
  return {
@@ -172,7 +701,8 @@ async def test_sienge_connection() -> Dict:
172
701
 
173
702
  @mcp.tool
174
703
  async def get_sienge_customers(
175
- limit: Optional[int] = 50, offset: Optional[int] = 0, search: Optional[str] = None, customer_type_id: Optional[str] = None
704
+ limit: Optional[int] = 50, offset: Optional[int] = 0, search: Optional[str] = None, customer_type_id: Optional[str] = None,
705
+ _meta: Optional[Dict] = None
176
706
  ) -> Dict:
177
707
  """
178
708
  Busca clientes no Sienge com filtros
@@ -183,14 +713,13 @@ async def get_sienge_customers(
183
713
  search: Buscar por nome ou documento
184
714
  customer_type_id: Filtrar por tipo de cliente
185
715
  """
186
- params = {"limit": min(limit or 50, 200), "offset": offset or 0}
187
-
188
- if search:
189
- params["search"] = search
190
- if customer_type_id:
191
- params["customer_type_id"] = customer_type_id
192
-
193
- result = await make_sienge_request("GET", "/customers", params=params)
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
+ )
194
723
 
195
724
  if result["success"]:
196
725
  data = result["data"]
@@ -203,7 +732,13 @@ async def get_sienge_customers(
203
732
  "message": f"✅ Encontrados {len(customers)} clientes (total: {total_count})",
204
733
  "customers": customers,
205
734
  "count": len(customers),
206
- "filters_applied": params,
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")
207
742
  }
208
743
 
209
744
  return {
@@ -211,13 +746,15 @@ async def get_sienge_customers(
211
746
  "message": "❌ Erro ao buscar clientes",
212
747
  "error": result.get("error"),
213
748
  "details": result.get("message"),
749
+ "request_id": result.get("request_id")
214
750
  }
215
751
 
216
752
 
217
753
  @mcp.tool
218
- async def get_sienge_customer_types() -> Dict:
754
+ async def get_sienge_customer_types(_meta: Optional[Dict] = None) -> Dict:
219
755
  """Lista tipos de clientes disponíveis"""
220
- result = await make_sienge_request("GET", "/customer-types")
756
+ # Usar serviço interno
757
+ result = await _svc_get_customer_types()
221
758
 
222
759
  if result["success"]:
223
760
  data = result["data"]
@@ -230,6 +767,10 @@ async def get_sienge_customer_types() -> Dict:
230
767
  "message": f"✅ Encontrados {len(customer_types)} tipos de clientes (total: {total_count})",
231
768
  "customer_types": customer_types,
232
769
  "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")
233
774
  }
234
775
 
235
776
  return {
@@ -237,6 +778,195 @@ async def get_sienge_customer_types() -> Dict:
237
778
  "message": "❌ Erro ao buscar tipos de clientes",
238
779
  "error": result.get("error"),
239
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
+ 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
+ }
963
+
964
+ return {
965
+ "success": False,
966
+ "message": "❌ Erro ao buscar solicitações de compra",
967
+ "error": result.get("error"),
968
+ "details": result.get("message"),
969
+ "request_id": result.get("request_id")
240
970
  }
241
971
 
242
972
 
@@ -244,7 +974,8 @@ async def get_sienge_customer_types() -> Dict:
244
974
 
245
975
 
246
976
  @mcp.tool
247
- async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int] = 0, search: Optional[str] = None) -> Dict:
977
+ async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int] = 0, search: Optional[str] = None,
978
+ _meta: Optional[Dict] = None) -> Dict:
248
979
  """
249
980
  Busca credores/fornecedores
250
981
 
@@ -253,11 +984,12 @@ async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int]
253
984
  offset: Pular registros (padrão: 0)
254
985
  search: Buscar por nome
255
986
  """
256
- params = {"limit": min(limit or 50, 200), "offset": offset or 0}
257
- if search:
258
- params["search"] = search
259
-
260
- result = await make_sienge_request("GET", "/creditors", params=params)
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
+ )
261
993
 
262
994
  if result["success"]:
263
995
  data = result["data"]
@@ -270,6 +1002,11 @@ async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int]
270
1002
  "message": f"✅ Encontrados {len(creditors)} credores (total: {total_count})",
271
1003
  "creditors": creditors,
272
1004
  "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")
273
1010
  }
274
1011
 
275
1012
  return {
@@ -277,18 +1014,21 @@ async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int]
277
1014
  "message": "❌ Erro ao buscar credores",
278
1015
  "error": result.get("error"),
279
1016
  "details": result.get("message"),
1017
+ "request_id": result.get("request_id")
280
1018
  }
281
1019
 
282
1020
 
283
1021
  @mcp.tool
284
- async def get_sienge_creditor_bank_info(creditor_id: str) -> Dict:
1022
+ async def get_sienge_creditor_bank_info(creditor_id: str,
1023
+ _meta: Optional[Dict] = None) -> Dict:
285
1024
  """
286
1025
  Consulta informações bancárias de um credor
287
1026
 
288
1027
  Args:
289
1028
  creditor_id: ID do credor (obrigatório)
290
1029
  """
291
- result = await make_sienge_request("GET", f"/creditors/{creditor_id}/bank-informations")
1030
+ # Usar serviço interno
1031
+ result = await _svc_get_creditor_bank_info(creditor_id=creditor_id)
292
1032
 
293
1033
  if result["success"]:
294
1034
  return {
@@ -296,6 +1036,8 @@ async def get_sienge_creditor_bank_info(creditor_id: str) -> Dict:
296
1036
  "message": f"✅ Informações bancárias do credor {creditor_id}",
297
1037
  "creditor_id": creditor_id,
298
1038
  "bank_info": result["data"],
1039
+ "request_id": result.get("request_id"),
1040
+ "latency_ms": result.get("latency_ms")
299
1041
  }
300
1042
 
301
1043
  return {
@@ -303,6 +1045,7 @@ async def get_sienge_creditor_bank_info(creditor_id: str) -> Dict:
303
1045
  "message": f"❌ Erro ao buscar info bancária do credor {creditor_id}",
304
1046
  "error": result.get("error"),
305
1047
  "details": result.get("message"),
1048
+ "request_id": result.get("request_id")
306
1049
  }
307
1050
 
308
1051
 
@@ -323,9 +1066,10 @@ async def get_sienge_accounts_receivable(
323
1066
  origins_ids: Optional[List[str]] = None,
324
1067
  bearers_id_in: Optional[List[int]] = None,
325
1068
  bearers_id_not_in: Optional[List[int]] = None,
326
- ) -> Dict:
1069
+ _meta: Optional[Dict] = None) -> Dict:
327
1070
  """
328
1071
  Consulta parcelas do contas a receber via API bulk-data
1072
+ MELHORADO: Suporte a polling assíncrono para requests 202
329
1073
 
330
1074
  Args:
331
1075
  start_date: Data de início do período (YYYY-MM-DD) - OBRIGATÓRIO
@@ -341,90 +1085,121 @@ async def get_sienge_accounts_receivable(
341
1085
  bearers_id_in: Filtrar parcelas com códigos de portador específicos
342
1086
  bearers_id_not_in: Filtrar parcelas excluindo códigos de portador específicos
343
1087
  """
344
- params = {"startDate": start_date, "endDate": end_date, "selectionType": selection_type}
345
-
346
- if company_id:
347
- params["companyId"] = company_id
348
- if cost_centers_id:
349
- params["costCentersId"] = cost_centers_id
350
- if correction_indexer_id:
351
- params["correctionIndexerId"] = correction_indexer_id
352
- if correction_date:
353
- params["correctionDate"] = correction_date
354
- if change_start_date:
355
- params["changeStartDate"] = change_start_date
356
- if completed_bills:
357
- params["completedBills"] = completed_bills
358
- if origins_ids:
359
- params["originsIds"] = origins_ids
360
- if bearers_id_in:
361
- params["bearersIdIn"] = bearers_id_in
362
- if bearers_id_not_in:
363
- params["bearersIdNotIn"] = bearers_id_not_in
364
-
365
- result = await make_sienge_bulk_request("GET", "/income", params=params)
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
+ )
366
1103
 
367
1104
  if result["success"]:
368
- data = result["data"]
369
- income_data = data.get("data", []) if isinstance(data, dict) else data
370
-
371
- return {
1105
+ # Para requests normais (200) e assíncronos processados
1106
+ income_data = result.get("data", [])
1107
+
1108
+ response = {
372
1109
  "success": True,
373
1110
  "message": f"✅ Encontradas {len(income_data)} parcelas a receber",
374
1111
  "income_data": income_data,
375
1112
  "count": len(income_data),
376
1113
  "period": f"{start_date} a {end_date}",
377
1114
  "selection_type": selection_type,
378
- "filters": params,
1115
+ "request_id": result.get("request_id"),
1116
+ "latency_ms": result.get("latency_ms")
379
1117
  }
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
380
1132
 
381
1133
  return {
382
1134
  "success": False,
383
1135
  "message": "❌ Erro ao buscar parcelas a receber",
384
1136
  "error": result.get("error"),
385
1137
  "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
386
1143
  }
387
1144
 
388
1145
 
389
1146
  @mcp.tool
390
1147
  async def get_sienge_accounts_receivable_by_bills(
391
- bills_ids: List[int], correction_indexer_id: Optional[int] = None, correction_date: Optional[str] = None
392
- ) -> Dict:
1148
+ bills_ids: List[int], correction_indexer_id: Optional[int] = None, correction_date: Optional[str] = None,
1149
+ _meta: Optional[Dict] = None) -> Dict:
393
1150
  """
394
1151
  Consulta parcelas dos títulos informados via API bulk-data
1152
+ MELHORADO: Suporte a polling assíncrono para requests 202
395
1153
 
396
1154
  Args:
397
1155
  bills_ids: Lista de códigos dos títulos - OBRIGATÓRIO
398
1156
  correction_indexer_id: Código do indexador de correção
399
1157
  correction_date: Data para correção do indexador (YYYY-MM-DD)
400
1158
  """
401
- params = {"billsIds": bills_ids}
402
-
403
- if correction_indexer_id:
404
- params["correctionIndexerId"] = correction_indexer_id
405
- if correction_date:
406
- params["correctionDate"] = correction_date
407
-
408
- result = await make_sienge_bulk_request("GET", "/income/by-bills", params=params)
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
+ )
409
1165
 
410
1166
  if result["success"]:
411
- data = result["data"]
412
- income_data = data.get("data", []) if isinstance(data, dict) else data
413
-
414
- return {
1167
+ income_data = result.get("data", [])
1168
+
1169
+ response = {
415
1170
  "success": True,
416
1171
  "message": f"✅ Encontradas {len(income_data)} parcelas dos títulos informados",
417
1172
  "income_data": income_data,
418
1173
  "count": len(income_data),
419
1174
  "bills_consulted": bills_ids,
420
- "filters": params,
1175
+ "request_id": result.get("request_id"),
1176
+ "latency_ms": result.get("latency_ms")
421
1177
  }
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
422
1192
 
423
1193
  return {
424
1194
  "success": False,
425
1195
  "message": "❌ Erro ao buscar parcelas dos títulos informados",
426
1196
  "error": result.get("error"),
427
1197
  "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
428
1203
  }
429
1204
 
430
1205
 
@@ -435,7 +1210,7 @@ async def get_sienge_bills(
435
1210
  creditor_id: Optional[str] = None,
436
1211
  status: Optional[str] = None,
437
1212
  limit: Optional[int] = 50,
438
- ) -> Dict:
1213
+ _meta: Optional[Dict] = None) -> Dict:
439
1214
  """
440
1215
  Consulta títulos a pagar (contas a pagar) - REQUER startDate obrigatório
441
1216
 
@@ -446,26 +1221,14 @@ async def get_sienge_bills(
446
1221
  status: Status do título (ex: open, paid, cancelled)
447
1222
  limit: Máximo de registros (padrão: 50, máx: 200)
448
1223
  """
449
- from datetime import datetime, timedelta
450
-
451
- # Se start_date não fornecido, usar últimos 30 dias
452
- if not start_date:
453
- start_date = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
454
-
455
- # Se end_date não fornecido, usar hoje
456
- if not end_date:
457
- end_date = datetime.now().strftime("%Y-%m-%d")
458
-
459
- # Parâmetros obrigatórios
460
- params = {"startDate": start_date, "endDate": end_date, "limit": min(limit or 50, 200)} # OBRIGATÓRIO pela API
461
-
462
- # Parâmetros opcionais
463
- if creditor_id:
464
- params["creditor_id"] = creditor_id
465
- if status:
466
- params["status"] = status
467
-
468
- result = await make_sienge_request("GET", "/bills", params=params)
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
+ )
469
1232
 
470
1233
  if result["success"]:
471
1234
  data = result["data"]
@@ -473,14 +1236,33 @@ async def get_sienge_bills(
473
1236
  metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
474
1237
  total_count = metadata.get("count", len(bills))
475
1238
 
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
+
476
1252
  return {
477
1253
  "success": True,
478
- "message": f"✅ Encontrados {len(bills)} títulos a pagar (total: {total_count}) - período: {start_date} a {end_date}",
1254
+ "message": f"✅ Encontrados {len(bills)} títulos a pagar (total: {total_count}) - período: {actual_start} a {actual_end}",
479
1255
  "bills": bills,
480
1256
  "count": len(bills),
481
1257
  "total_count": total_count,
482
- "period": {"start_date": start_date, "end_date": end_date},
483
- "filters": params,
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")
484
1266
  }
485
1267
 
486
1268
  return {
@@ -488,6 +1270,7 @@ async def get_sienge_bills(
488
1270
  "message": "❌ Erro ao buscar títulos a pagar",
489
1271
  "error": result.get("error"),
490
1272
  "details": result.get("message"),
1273
+ "request_id": result.get("request_id")
491
1274
  }
492
1275
 
493
1276
 
@@ -500,7 +1283,7 @@ async def get_sienge_purchase_orders(
500
1283
  status: Optional[str] = None,
501
1284
  date_from: Optional[str] = None,
502
1285
  limit: Optional[int] = 50,
503
- ) -> Dict:
1286
+ _meta: Optional[Dict] = None) -> Dict:
504
1287
  """
505
1288
  Consulta pedidos de compra
506
1289
 
@@ -533,6 +1316,8 @@ async def get_sienge_purchase_orders(
533
1316
  "message": f"✅ Encontrados {len(orders)} pedidos de compra",
534
1317
  "purchase_orders": orders,
535
1318
  "count": len(orders),
1319
+ "request_id": result.get("request_id"),
1320
+ "latency_ms": result.get("latency_ms")
536
1321
  }
537
1322
 
538
1323
  return {
@@ -540,11 +1325,14 @@ async def get_sienge_purchase_orders(
540
1325
  "message": "❌ Erro ao buscar pedidos de compra",
541
1326
  "error": result.get("error"),
542
1327
  "details": result.get("message"),
1328
+ "request_id": result.get("request_id"),
1329
+ "latency_ms": result.get("latency_ms")
543
1330
  }
544
1331
 
545
1332
 
546
1333
  @mcp.tool
547
- async def get_sienge_purchase_order_items(purchase_order_id: str) -> Dict:
1334
+ async def get_sienge_purchase_order_items(purchase_order_id: str,
1335
+ _meta: Optional[Dict] = None) -> Dict:
548
1336
  """
549
1337
  Consulta itens de um pedido de compra específico
550
1338
 
@@ -574,7 +1362,8 @@ async def get_sienge_purchase_order_items(purchase_order_id: str) -> Dict:
574
1362
 
575
1363
 
576
1364
  @mcp.tool
577
- async def get_sienge_purchase_requests(purchase_request_id: Optional[str] = None, limit: Optional[int] = 50) -> Dict:
1365
+ async def get_sienge_purchase_requests(purchase_request_id: Optional[str] = None, limit: Optional[int] = 50,
1366
+ _meta: Optional[Dict] = None) -> Dict:
578
1367
  """
579
1368
  Consulta solicitações de compra
580
1369
 
@@ -615,7 +1404,8 @@ async def get_sienge_purchase_requests(purchase_request_id: Optional[str] = None
615
1404
 
616
1405
 
617
1406
  @mcp.tool
618
- async def create_sienge_purchase_request(description: str, project_id: str, items: List[Dict[str, Any]]) -> Dict:
1407
+ async def create_sienge_purchase_request(description: str, project_id: str, items: List[Dict[str, Any]],
1408
+ _meta: Optional[Dict] = None) -> Dict:
619
1409
  """
620
1410
  Cria nova solicitação de compra
621
1411
 
@@ -631,14 +1421,18 @@ async def create_sienge_purchase_request(description: str, project_id: str, item
631
1421
  "date": datetime.now().strftime("%Y-%m-%d"),
632
1422
  }
633
1423
 
634
- result = await make_sienge_request("POST", "/purchase-requests", json_data=request_data)
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)
635
1427
 
636
1428
  if result["success"]:
637
1429
  return {
638
1430
  "success": True,
639
1431
  "message": "✅ Solicitação de compra criada com sucesso",
640
- "request_id": result["data"].get("id"),
1432
+ "request_id": result.get("request_id"),
1433
+ "purchase_request_id": result["data"].get("id"),
641
1434
  "data": result["data"],
1435
+ "latency_ms": result.get("latency_ms")
642
1436
  }
643
1437
 
644
1438
  return {
@@ -646,6 +1440,7 @@ async def create_sienge_purchase_request(description: str, project_id: str, item
646
1440
  "message": "❌ Erro ao criar solicitação de compra",
647
1441
  "error": result.get("error"),
648
1442
  "details": result.get("message"),
1443
+ "request_id": result.get("request_id")
649
1444
  }
650
1445
 
651
1446
 
@@ -653,7 +1448,8 @@ async def create_sienge_purchase_request(description: str, project_id: str, item
653
1448
 
654
1449
 
655
1450
  @mcp.tool
656
- async def get_sienge_purchase_invoice(sequential_number: int) -> Dict:
1451
+ async def get_sienge_purchase_invoice(sequential_number: int,
1452
+ _meta: Optional[Dict] = None) -> Dict:
657
1453
  """
658
1454
  Consulta nota fiscal de compra por número sequencial
659
1455
 
@@ -674,7 +1470,8 @@ async def get_sienge_purchase_invoice(sequential_number: int) -> Dict:
674
1470
 
675
1471
 
676
1472
  @mcp.tool
677
- async def get_sienge_purchase_invoice_items(sequential_number: int) -> Dict:
1473
+ async def get_sienge_purchase_invoice_items(sequential_number: int,
1474
+ _meta: Optional[Dict] = None) -> Dict:
678
1475
  """
679
1476
  Consulta itens de uma nota fiscal de compra
680
1477
 
@@ -715,7 +1512,7 @@ async def create_sienge_purchase_invoice(
715
1512
  issue_date: str,
716
1513
  series: Optional[str] = None,
717
1514
  notes: Optional[str] = None,
718
- ) -> Dict:
1515
+ _meta: Optional[Dict] = None) -> Dict:
719
1516
  """
720
1517
  Cadastra uma nova nota fiscal de compra
721
1518
 
@@ -731,13 +1528,13 @@ async def create_sienge_purchase_invoice(
731
1528
  notes: Observações (opcional)
732
1529
  """
733
1530
  invoice_data = {
734
- "documentId": document_id,
1531
+ "document_id": document_id,
735
1532
  "number": number,
736
- "supplierId": supplier_id,
737
- "companyId": company_id,
738
- "movementTypeId": movement_type_id,
739
- "movementDate": movement_date,
740
- "issueDate": issue_date,
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,
741
1538
  }
742
1539
 
743
1540
  if series:
@@ -745,7 +1542,7 @@ async def create_sienge_purchase_invoice(
745
1542
  if notes:
746
1543
  invoice_data["notes"] = notes
747
1544
 
748
- result = await make_sienge_request("POST", "/purchase-invoices", json_data=invoice_data)
1545
+ result = await make_sienge_request("POST", "/purchase-invoices", json_data=to_camel_json(invoice_data))
749
1546
 
750
1547
  if result["success"]:
751
1548
  return {"success": True, "message": f"✅ Nota fiscal {number} criada com sucesso", "invoice": result["data"]}
@@ -765,7 +1562,7 @@ async def add_items_to_purchase_invoice(
765
1562
  copy_notes_purchase_orders: bool = True,
766
1563
  copy_notes_resources: bool = False,
767
1564
  copy_attachments_purchase_orders: bool = True,
768
- ) -> Dict:
1565
+ _meta: Optional[Dict] = None) -> Dict:
769
1566
  """
770
1567
  Insere itens em uma nota fiscal a partir de entregas de pedidos de compra
771
1568
 
@@ -782,14 +1579,14 @@ async def add_items_to_purchase_invoice(
782
1579
  copy_attachments_purchase_orders: Copiar anexos dos pedidos de compra
783
1580
  """
784
1581
  item_data = {
785
- "deliveriesOrder": deliveries_order,
786
- "copyNotesPurchaseOrders": copy_notes_purchase_orders,
787
- "copyNotesResources": copy_notes_resources,
788
- "copyAttachmentsPurchaseOrders": copy_attachments_purchase_orders,
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,
789
1586
  }
790
1587
 
791
1588
  result = await make_sienge_request(
792
- "POST", f"/purchase-invoices/{sequential_number}/items/purchase-orders/delivery-schedules", json_data=item_data
1589
+ "POST", f"/purchase-invoices/{sequential_number}/items/purchase-orders/delivery-schedules", json_data=to_camel_json(item_data)
793
1590
  )
794
1591
 
795
1592
  if result["success"]:
@@ -816,7 +1613,7 @@ async def get_sienge_purchase_invoices_deliveries_attended(
816
1613
  purchase_order_item_number: Optional[int] = None,
817
1614
  limit: Optional[int] = 100,
818
1615
  offset: Optional[int] = 0,
819
- ) -> Dict:
1616
+ _meta: Optional[Dict] = None) -> Dict:
820
1617
  """
821
1618
  Lista entregas atendidas entre pedidos de compra e notas fiscais
822
1619
 
@@ -870,7 +1667,8 @@ async def get_sienge_purchase_invoices_deliveries_attended(
870
1667
 
871
1668
 
872
1669
  @mcp.tool
873
- async def get_sienge_stock_inventory(cost_center_id: str, resource_id: Optional[str] = None) -> Dict:
1670
+ async def get_sienge_stock_inventory(cost_center_id: str, resource_id: Optional[str] = None,
1671
+ _meta: Optional[Dict] = None) -> Dict:
874
1672
  """
875
1673
  Consulta inventário de estoque por centro de custo
876
1674
 
@@ -907,7 +1705,8 @@ async def get_sienge_stock_inventory(cost_center_id: str, resource_id: Optional[
907
1705
 
908
1706
 
909
1707
  @mcp.tool
910
- async def get_sienge_stock_reservations(limit: Optional[int] = 50) -> Dict:
1708
+ async def get_sienge_stock_reservations(limit: Optional[int] = 50,
1709
+ _meta: Optional[Dict] = None) -> Dict:
911
1710
  """
912
1711
  Lista reservas de estoque
913
1712
 
@@ -947,9 +1746,10 @@ async def get_sienge_projects(
947
1746
  enterprise_type: Optional[int] = None,
948
1747
  receivable_register: Optional[str] = None,
949
1748
  only_buildings_enabled: Optional[bool] = False,
950
- ) -> Dict:
1749
+ _meta: Optional[Dict] = None) -> Dict:
951
1750
  """
952
1751
  Busca empreendimentos/obras no Sienge
1752
+ CORRIGIDO: Mapeamento correto da chave de resposta
953
1753
 
954
1754
  Args:
955
1755
  limit: Máximo de registros (padrão: 100, máximo: 200)
@@ -959,31 +1759,39 @@ async def get_sienge_projects(
959
1759
  receivable_register: Filtro de registro de recebíveis (B3, CERC)
960
1760
  only_buildings_enabled: Retornar apenas obras habilitadas para integração orçamentária
961
1761
  """
962
- params = {"limit": min(limit or 100, 200), "offset": offset or 0}
963
-
964
- if company_id:
965
- params["companyId"] = company_id
966
- if enterprise_type:
967
- params["type"] = enterprise_type
968
- if receivable_register:
969
- params["receivableRegister"] = receivable_register
970
- if only_buildings_enabled:
971
- params["onlyBuildingsEnabledForIntegration"] = only_buildings_enabled
972
-
973
- result = await make_sienge_request("GET", "/enterprises", params=params)
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
+ )
974
1771
 
975
1772
  if result["success"]:
976
1773
  data = result["data"]
1774
+ # CORREÇÃO: API retorna em "results", mas paginador espera em "enterprises"
977
1775
  enterprises = data.get("results", []) if isinstance(data, dict) else data
978
1776
  metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
1777
+ total_count = metadata.get("count", len(enterprises))
979
1778
 
980
1779
  return {
981
1780
  "success": True,
982
- "message": f"✅ Encontrados {len(enterprises)} empreendimentos",
983
- "enterprises": enterprises,
1781
+ "message": f"✅ Encontrados {len(enterprises)} empreendimentos (total: {total_count})",
1782
+ "enterprises": enterprises, # Manter consistência para paginador
1783
+ "projects": enterprises, # Alias para compatibilidade
984
1784
  "count": len(enterprises),
1785
+ "total_count": total_count,
985
1786
  "metadata": metadata,
986
- "filters": params,
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")
987
1795
  }
988
1796
 
989
1797
  return {
@@ -991,11 +1799,13 @@ async def get_sienge_projects(
991
1799
  "message": "❌ Erro ao buscar empreendimentos",
992
1800
  "error": result.get("error"),
993
1801
  "details": result.get("message"),
1802
+ "request_id": result.get("request_id")
994
1803
  }
995
1804
 
996
1805
 
997
1806
  @mcp.tool
998
- async def get_sienge_enterprise_by_id(enterprise_id: int) -> Dict:
1807
+ async def get_sienge_enterprise_by_id(enterprise_id: int,
1808
+ _meta: Optional[Dict] = None) -> Dict:
999
1809
  """
1000
1810
  Busca um empreendimento específico por ID no Sienge
1001
1811
 
@@ -1016,7 +1826,8 @@ async def get_sienge_enterprise_by_id(enterprise_id: int) -> Dict:
1016
1826
 
1017
1827
 
1018
1828
  @mcp.tool
1019
- async def get_sienge_enterprise_groupings(enterprise_id: int) -> Dict:
1829
+ async def get_sienge_enterprise_groupings(enterprise_id: int,
1830
+ _meta: Optional[Dict] = None) -> Dict:
1020
1831
  """
1021
1832
  Busca agrupamentos de unidades de um empreendimento específico
1022
1833
 
@@ -1043,7 +1854,8 @@ async def get_sienge_enterprise_groupings(enterprise_id: int) -> Dict:
1043
1854
 
1044
1855
 
1045
1856
  @mcp.tool
1046
- async def get_sienge_units(limit: Optional[int] = 50, offset: Optional[int] = 0) -> Dict:
1857
+ async def get_sienge_units(limit: Optional[int] = 50, offset: Optional[int] = 0,
1858
+ _meta: Optional[Dict] = None) -> Dict:
1047
1859
  """
1048
1860
  Consulta unidades cadastradas no Sienge
1049
1861
 
@@ -1065,6 +1877,9 @@ async def get_sienge_units(limit: Optional[int] = 50, offset: Optional[int] = 0)
1065
1877
  "message": f"✅ Encontradas {len(units)} unidades (total: {total_count})",
1066
1878
  "units": units,
1067
1879
  "count": len(units),
1880
+ "total_count": total_count,
1881
+ "request_id": result.get("request_id"),
1882
+ "latency_ms": result.get("latency_ms")
1068
1883
  }
1069
1884
 
1070
1885
  return {
@@ -1072,6 +1887,8 @@ async def get_sienge_units(limit: Optional[int] = 50, offset: Optional[int] = 0)
1072
1887
  "message": "❌ Erro ao buscar unidades",
1073
1888
  "error": result.get("error"),
1074
1889
  "details": result.get("message"),
1890
+ "request_id": result.get("request_id"),
1891
+ "latency_ms": result.get("latency_ms")
1075
1892
  }
1076
1893
 
1077
1894
 
@@ -1084,7 +1901,7 @@ async def get_sienge_unit_cost_tables(
1084
1901
  description: Optional[str] = None,
1085
1902
  status: Optional[str] = "Active",
1086
1903
  integration_enabled: Optional[bool] = None,
1087
- ) -> Dict:
1904
+ _meta: Optional[Dict] = None) -> Dict:
1088
1905
  """
1089
1906
  Consulta tabelas de custos unitários
1090
1907
 
@@ -1132,8 +1949,8 @@ async def search_sienge_data(
1132
1949
  query: str,
1133
1950
  entity_type: Optional[str] = None,
1134
1951
  limit: Optional[int] = 20,
1135
- filters: Optional[Dict[str, Any]] = None
1136
- ) -> Dict:
1952
+ filters: Optional[Dict[str, Any]] = None,
1953
+ _meta: Optional[Dict] = None) -> Dict:
1137
1954
  """
1138
1955
  Busca universal no Sienge - compatível com ChatGPT/OpenAI MCP
1139
1956
 
@@ -1210,35 +2027,44 @@ async def search_sienge_data(
1210
2027
 
1211
2028
 
1212
2029
  async def _search_specific_entity(entity_type: str, query: str, limit: int, filters: Dict) -> Dict:
1213
- """Função auxiliar para buscar em uma entidade específica"""
2030
+ """
2031
+ Função auxiliar para buscar em uma entidade específica
2032
+ CORRIGIDO: Usa serviços internos, nunca outras tools
2033
+ """
1214
2034
 
1215
2035
  if entity_type == "customers":
1216
- result = await get_sienge_customers(limit=limit, search=query)
2036
+ result = await _svc_get_customers(limit=limit, search=query)
1217
2037
  if result["success"]:
2038
+ data = result["data"]
2039
+ customers = data.get("results", []) if isinstance(data, dict) else data
1218
2040
  return {
1219
2041
  "success": True,
1220
- "data": result["customers"],
1221
- "count": result["count"],
2042
+ "data": customers,
2043
+ "count": len(customers),
1222
2044
  "entity_type": "customers"
1223
2045
  }
1224
2046
 
1225
2047
  elif entity_type == "creditors":
1226
- result = await get_sienge_creditors(limit=limit, search=query)
2048
+ result = await _svc_get_creditors(limit=limit, search=query)
1227
2049
  if result["success"]:
2050
+ data = result["data"]
2051
+ creditors = data.get("results", []) if isinstance(data, dict) else data
1228
2052
  return {
1229
2053
  "success": True,
1230
- "data": result["creditors"],
1231
- "count": result["count"],
2054
+ "data": creditors,
2055
+ "count": len(creditors),
1232
2056
  "entity_type": "creditors"
1233
2057
  }
1234
2058
 
1235
2059
  elif entity_type == "projects" or entity_type == "enterprises":
1236
2060
  # Para projetos, usar filtros mais específicos se disponível
1237
2061
  company_id = filters.get("company_id")
1238
- result = await get_sienge_projects(limit=limit, company_id=company_id)
2062
+ result = await _svc_get_projects(limit=limit, company_id=company_id)
1239
2063
  if result["success"]:
2064
+ data = result["data"]
2065
+ projects = data.get("results", []) if isinstance(data, dict) else data
2066
+
1240
2067
  # Filtrar por query se fornecida
1241
- projects = result["enterprises"]
1242
2068
  if query:
1243
2069
  projects = [
1244
2070
  p for p in projects
@@ -1257,23 +2083,27 @@ async def _search_specific_entity(entity_type: str, query: str, limit: int, filt
1257
2083
  # Para títulos, usar data padrão se não especificada
1258
2084
  start_date = filters.get("start_date")
1259
2085
  end_date = filters.get("end_date")
1260
- result = await get_sienge_bills(
2086
+ result = await _svc_get_bills(
1261
2087
  start_date=start_date,
1262
2088
  end_date=end_date,
1263
2089
  limit=limit
1264
2090
  )
1265
2091
  if result["success"]:
2092
+ data = result["data"]
2093
+ bills = data.get("results", []) if isinstance(data, dict) else data
1266
2094
  return {
1267
2095
  "success": True,
1268
- "data": result["bills"],
1269
- "count": result["count"],
2096
+ "data": bills,
2097
+ "count": len(bills),
1270
2098
  "entity_type": "bills"
1271
2099
  }
1272
2100
 
1273
2101
  elif entity_type == "purchase_orders":
1274
- result = await get_sienge_purchase_orders(limit=limit)
2102
+ result = await _svc_get_purchase_orders(limit=limit)
1275
2103
  if result["success"]:
1276
- orders = result["purchase_orders"]
2104
+ data = result["data"]
2105
+ orders = data.get("results", []) if isinstance(data, dict) else data
2106
+
1277
2107
  # Filtrar por query se fornecida
1278
2108
  if query:
1279
2109
  orders = [
@@ -1297,7 +2127,7 @@ async def _search_specific_entity(entity_type: str, query: str, limit: int, filt
1297
2127
 
1298
2128
 
1299
2129
  @mcp.tool
1300
- async def list_sienge_entities() -> Dict:
2130
+ async def list_sienge_entities(_meta: Optional[Dict] = None) -> Dict:
1301
2131
  """
1302
2132
  Lista todas as entidades disponíveis no Sienge MCP para busca
1303
2133
 
@@ -1316,28 +2146,28 @@ async def list_sienge_entities() -> Dict:
1316
2146
  "name": "Credores/Fornecedores",
1317
2147
  "description": "Fornecedores e credores cadastrados",
1318
2148
  "search_fields": ["nome", "documento"],
1319
- "tools": ["get_sienge_creditors", "get_sienge_creditor_bank_info"]
2149
+ "tools": ["get_sienge_creditors", "get_sienge_creditor_bank_info", "get_sienge_suppliers"]
1320
2150
  },
1321
2151
  {
1322
2152
  "type": "projects",
1323
2153
  "name": "Empreendimentos/Obras",
1324
2154
  "description": "Projetos e obras cadastrados",
1325
2155
  "search_fields": ["código", "descrição", "nome"],
1326
- "tools": ["get_sienge_projects", "get_sienge_enterprise_by_id"]
2156
+ "tools": ["get_sienge_projects", "get_sienge_enterprises", "get_sienge_enterprise_by_id"]
1327
2157
  },
1328
2158
  {
1329
2159
  "type": "bills",
1330
2160
  "name": "Títulos a Pagar",
1331
2161
  "description": "Contas a pagar e títulos financeiros",
1332
2162
  "search_fields": ["número", "credor", "valor"],
1333
- "tools": ["get_sienge_bills"]
2163
+ "tools": ["get_sienge_bills", "get_sienge_accounts_payable"]
1334
2164
  },
1335
2165
  {
1336
2166
  "type": "purchase_orders",
1337
2167
  "name": "Pedidos de Compra",
1338
2168
  "description": "Pedidos de compra e solicitações",
1339
2169
  "search_fields": ["id", "descrição", "status"],
1340
- "tools": ["get_sienge_purchase_orders", "get_sienge_purchase_requests"]
2170
+ "tools": ["get_sienge_purchase_orders", "get_sienge_purchase_requests", "list_sienge_purchase_requests"]
1341
2171
  },
1342
2172
  {
1343
2173
  "type": "invoices",
@@ -1358,7 +2188,14 @@ async def list_sienge_entities() -> Dict:
1358
2188
  "name": "Financeiro",
1359
2189
  "description": "Contas a receber e movimentações financeiras",
1360
2190
  "search_fields": ["período", "cliente", "valor"],
1361
- "tools": ["get_sienge_accounts_receivable"]
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"]
1362
2199
  }
1363
2200
  ]
1364
2201
 
@@ -1384,8 +2221,8 @@ async def get_sienge_data_paginated(
1384
2221
  page: int = 1,
1385
2222
  page_size: int = 20,
1386
2223
  filters: Optional[Dict[str, Any]] = None,
1387
- sort_by: Optional[str] = None
1388
- ) -> Dict:
2224
+ sort_by: Optional[str] = None,
2225
+ _meta: Optional[Dict] = None) -> Dict:
1389
2226
  """
1390
2227
  Busca dados do Sienge com paginação avançada - compatível com ChatGPT
1391
2228
 
@@ -1401,41 +2238,70 @@ async def get_sienge_data_paginated(
1401
2238
 
1402
2239
  filters = filters or {}
1403
2240
 
1404
- # Mapear para os tools existentes com offset
2241
+ # CORRIGIDO: Mapear para serviços internos, não tools
1405
2242
  if entity_type == "customers":
1406
2243
  search = filters.get("search")
1407
2244
  customer_type_id = filters.get("customer_type_id")
1408
- result = await get_sienge_customers(
2245
+ result = await _svc_get_customers(
1409
2246
  limit=page_size,
1410
2247
  offset=offset,
1411
2248
  search=search,
1412
2249
  customer_type_id=customer_type_id
1413
2250
  )
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
1414
2258
 
1415
2259
  elif entity_type == "creditors":
1416
2260
  search = filters.get("search")
1417
- result = await get_sienge_creditors(
2261
+ result = await _svc_get_creditors(
1418
2262
  limit=page_size,
1419
2263
  offset=offset,
1420
2264
  search=search
1421
2265
  )
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
1422
2273
 
1423
2274
  elif entity_type == "projects":
1424
- result = await get_sienge_projects(
2275
+ result = await _svc_get_projects(
1425
2276
  limit=page_size,
1426
2277
  offset=offset,
1427
2278
  company_id=filters.get("company_id"),
1428
2279
  enterprise_type=filters.get("enterprise_type")
1429
2280
  )
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
1430
2289
 
1431
2290
  elif entity_type == "bills":
1432
- result = await get_sienge_bills(
2291
+ result = await _svc_get_bills(
1433
2292
  start_date=filters.get("start_date"),
1434
2293
  end_date=filters.get("end_date"),
1435
2294
  creditor_id=filters.get("creditor_id"),
1436
2295
  status=filters.get("status"),
1437
2296
  limit=page_size
1438
2297
  )
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
1439
2305
 
1440
2306
  else:
1441
2307
  return {
@@ -1477,8 +2343,8 @@ async def search_sienge_financial_data(
1477
2343
  search_type: str = "both",
1478
2344
  amount_min: Optional[float] = None,
1479
2345
  amount_max: Optional[float] = None,
1480
- customer_creditor_search: Optional[str] = None
1481
- ) -> Dict:
2346
+ customer_creditor_search: Optional[str] = None,
2347
+ _meta: Optional[Dict] = None) -> Dict:
1482
2348
  """
1483
2349
  Busca avançada em dados financeiros do Sienge - Contas a Pagar e Receber
1484
2350
 
@@ -1499,20 +2365,21 @@ async def search_sienge_financial_data(
1499
2365
  # Buscar contas a receber
1500
2366
  if search_type in ["receivable", "both"]:
1501
2367
  try:
1502
- receivable_result = await get_sienge_accounts_receivable(
2368
+ # CORRIGIDO: Usar serviço interno
2369
+ receivable_result = await _svc_get_accounts_receivable(
1503
2370
  start_date=period_start,
1504
2371
  end_date=period_end,
1505
2372
  selection_type="D" # Por vencimento
1506
2373
  )
1507
2374
 
1508
2375
  if receivable_result["success"]:
1509
- receivable_data = receivable_result["income_data"]
2376
+ receivable_data = receivable_result.get("data", [])
1510
2377
 
1511
- # Aplicar filtros de valor se especificados
2378
+ # CORRIGIDO: Aplicar filtros de valor usando _parse_numeric_value
1512
2379
  if amount_min is not None or amount_max is not None:
1513
2380
  filtered_data = []
1514
2381
  for item in receivable_data:
1515
- amount = float(item.get("amount", 0) or 0)
2382
+ amount = _parse_numeric_value(item.get("amount", 0))
1516
2383
  if amount_min is not None and amount < amount_min:
1517
2384
  continue
1518
2385
  if amount_max is not None and amount > amount_max:
@@ -1545,20 +2412,22 @@ async def search_sienge_financial_data(
1545
2412
  # Buscar contas a pagar
1546
2413
  if search_type in ["payable", "both"]:
1547
2414
  try:
1548
- payable_result = await get_sienge_bills(
2415
+ # CORRIGIDO: Usar serviço interno
2416
+ payable_result = await _svc_get_bills(
1549
2417
  start_date=period_start,
1550
2418
  end_date=period_end,
1551
2419
  limit=100
1552
2420
  )
1553
2421
 
1554
2422
  if payable_result["success"]:
1555
- payable_data = payable_result["bills"]
2423
+ data = payable_result["data"]
2424
+ payable_data = data.get("results", []) if isinstance(data, dict) else data
1556
2425
 
1557
- # Aplicar filtros de valor se especificados
2426
+ # CORRIGIDO: Aplicar filtros de valor usando _parse_numeric_value
1558
2427
  if amount_min is not None or amount_max is not None:
1559
2428
  filtered_data = []
1560
2429
  for item in payable_data:
1561
- amount = float(item.get("amount", 0) or 0)
2430
+ amount = _parse_numeric_value(item.get("amount", 0))
1562
2431
  if amount_min is not None and amount < amount_min:
1563
2432
  continue
1564
2433
  if amount_max is not None and amount > amount_max:
@@ -1626,7 +2495,7 @@ async def search_sienge_financial_data(
1626
2495
 
1627
2496
 
1628
2497
  @mcp.tool
1629
- async def get_sienge_dashboard_summary() -> Dict:
2498
+ async def get_sienge_dashboard_summary(_meta: Optional[Dict] = None) -> Dict:
1630
2499
  """
1631
2500
  Obtém um resumo tipo dashboard com informações gerais do Sienge
1632
2501
  Útil para visão geral rápida do sistema
@@ -1650,23 +2519,26 @@ async def get_sienge_dashboard_summary() -> Dict:
1650
2519
 
1651
2520
  # 2. Contar clientes (amostra)
1652
2521
  try:
1653
- customers_result = await get_sienge_customers(limit=1)
1654
- if customers_result["success"]:
1655
- dashboard_data["customers_available"] = True
1656
- else:
1657
- dashboard_data["customers_available"] = False
2522
+ # CORRIGIDO: Usar serviço interno
2523
+ customers_result = await _svc_get_customers(limit=1)
2524
+ dashboard_data["customers"] = {"available": customers_result["success"]}
1658
2525
  except Exception as e:
1659
2526
  errors.append(f"Clientes: {str(e)}")
1660
- dashboard_data["customers_available"] = False
2527
+ dashboard_data["customers"] = {"available": False}
1661
2528
 
1662
2529
  # 3. Contar projetos (amostra)
1663
2530
  try:
1664
- projects_result = await get_sienge_projects(limit=5)
2531
+ # CORRIGIDO: Usar serviço interno
2532
+ projects_result = await _svc_get_projects(limit=5)
1665
2533
  if projects_result["success"]:
2534
+ data = projects_result["data"]
2535
+ enterprises = data.get("results", []) if isinstance(data, dict) else data
2536
+ metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
2537
+
1666
2538
  dashboard_data["projects"] = {
1667
2539
  "available": True,
1668
- "sample_count": len(projects_result["enterprises"]),
1669
- "total_count": projects_result.get("metadata", {}).get("count", "N/A")
2540
+ "sample_count": len(enterprises),
2541
+ "total_count": metadata.get("count", "N/A")
1670
2542
  }
1671
2543
  else:
1672
2544
  dashboard_data["projects"] = {"available": False}
@@ -1676,16 +2548,21 @@ async def get_sienge_dashboard_summary() -> Dict:
1676
2548
 
1677
2549
  # 4. Títulos a pagar do mês atual
1678
2550
  try:
1679
- bills_result = await get_sienge_bills(
2551
+ # CORRIGIDO: Usar serviço interno
2552
+ bills_result = await _svc_get_bills(
1680
2553
  start_date=current_month_start,
1681
2554
  end_date=current_month_end,
1682
2555
  limit=10
1683
2556
  )
1684
2557
  if bills_result["success"]:
2558
+ data = bills_result["data"]
2559
+ bills = data.get("results", []) if isinstance(data, dict) else data
2560
+ metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
2561
+
1685
2562
  dashboard_data["monthly_bills"] = {
1686
2563
  "available": True,
1687
- "count": len(bills_result["bills"]),
1688
- "total_count": bills_result.get("total_count", len(bills_result["bills"]))
2564
+ "count": len(bills),
2565
+ "total_count": metadata.get("count", len(bills))
1689
2566
  }
1690
2567
  else:
1691
2568
  dashboard_data["monthly_bills"] = {"available": False}
@@ -1695,11 +2572,15 @@ async def get_sienge_dashboard_summary() -> Dict:
1695
2572
 
1696
2573
  # 5. Tipos de clientes
1697
2574
  try:
1698
- customer_types_result = await get_sienge_customer_types()
2575
+ # CORRIGIDO: Usar serviço interno
2576
+ customer_types_result = await _svc_get_customer_types()
1699
2577
  if customer_types_result["success"]:
2578
+ data = customer_types_result["data"]
2579
+ customer_types = data.get("results", []) if isinstance(data, dict) else data
2580
+
1700
2581
  dashboard_data["customer_types"] = {
1701
2582
  "available": True,
1702
- "count": len(customer_types_result["customer_types"])
2583
+ "count": len(customer_types)
1703
2584
  }
1704
2585
  else:
1705
2586
  dashboard_data["customer_types"] = {"available": False}
@@ -1737,6 +2618,21 @@ def add(a: int, b: int) -> int:
1737
2618
  return a + b
1738
2619
 
1739
2620
 
2621
+ def _mask(s: str) -> str:
2622
+ """Mascara dados sensíveis mantendo apenas o início e fim"""
2623
+ if not s:
2624
+ return None
2625
+ if len(s) == 1:
2626
+ return s + "*"
2627
+ if len(s) == 2:
2628
+ return s
2629
+ if len(s) <= 4:
2630
+ return s[:2] + "*" * (len(s) - 2)
2631
+ # Para strings > 4: usar no máximo 4 asteriscos no meio
2632
+ middle_asterisks = min(4, len(s) - 4)
2633
+ return s[:2] + "*" * middle_asterisks + s[-2:]
2634
+
2635
+
1740
2636
  def _get_auth_info_internal() -> Dict:
1741
2637
  """Função interna para verificar configuração de autenticação"""
1742
2638
  if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
@@ -1747,7 +2643,7 @@ def _get_auth_info_internal() -> Dict:
1747
2643
  "configured": True,
1748
2644
  "base_url": SIENGE_BASE_URL,
1749
2645
  "subdomain": SIENGE_SUBDOMAIN,
1750
- "username": SIENGE_USERNAME,
2646
+ "username": _mask(SIENGE_USERNAME) if SIENGE_USERNAME else None,
1751
2647
  }
1752
2648
  else:
1753
2649
  return {