sienge-ecbiesek-mcp 1.1.1__py3-none-any.whl → 1.2.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,22 +2,32 @@
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
- import asyncio
10
- from typing import Dict, List, Optional, Any
11
- import json
16
+ from typing import Dict, List, Optional, Any, Union
12
17
  import os
13
18
  from dotenv import load_dotenv
14
- from datetime import datetime
15
- import base64
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
16
26
 
17
27
  # Carrega as variáveis de ambiente
18
28
  load_dotenv()
19
29
 
20
- mcp = FastMCP("Sienge API Integration 🏗️")
30
+ mcp = FastMCP("Sienge API Integration 🏗️ - ChatGPT Compatible")
21
31
 
22
32
  # Configurações da API do Sienge
23
33
  SIENGE_BASE_URL = os.getenv("SIENGE_BASE_URL", "https://api.sienge.com.br")
@@ -27,165 +37,635 @@ SIENGE_PASSWORD = os.getenv("SIENGE_PASSWORD", "")
27
37
  SIENGE_API_KEY = os.getenv("SIENGE_API_KEY", "")
28
38
  REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", "30"))
29
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
+
51
+
30
52
  class SiengeAPIError(Exception):
31
53
  """Exceção customizada para erros da API do Sienge"""
32
54
  pass
33
55
 
34
- async def make_sienge_request(method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None) -> Dict:
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
+
190
+ async def make_sienge_request(
191
+ method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None, use_cache: bool = True
192
+ ) -> Dict:
35
193
  """
36
194
  Função auxiliar para fazer requisições à API do Sienge (v1)
37
195
  Suporta tanto Bearer Token quanto Basic Auth
196
+ MELHORADO: Observabilidade, cache, normalização de parâmetros
38
197
  """
198
+ start_time = time.time()
199
+ req_id = str(uuid.uuid4())
200
+
39
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
+
40
214
  async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
41
215
  headers = {
42
- "Content-Type": "application/json",
43
- "Accept": "application/json"
216
+ "Content-Type": "application/json",
217
+ "Accept": "application/json",
218
+ "X-Request-ID": req_id
44
219
  }
45
-
46
- # Configurar autenticação e URL
220
+
221
+ # Configurar autenticação e URL (corrigindo URLs duplas)
47
222
  auth = None
48
-
223
+ base_normalized = _normalize_url(SIENGE_BASE_URL, SIENGE_SUBDOMAIN)
224
+
49
225
  if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
50
- # Bearer Token (Recomendado)
51
226
  headers["Authorization"] = f"Bearer {SIENGE_API_KEY}"
52
- url = f"{SIENGE_BASE_URL}/{SIENGE_SUBDOMAIN}/public/api/v1{endpoint}"
227
+ url = f"{base_normalized}/v1{endpoint}"
53
228
  elif SIENGE_USERNAME and SIENGE_PASSWORD:
54
- # Basic Auth usando httpx.BasicAuth
55
229
  auth = httpx.BasicAuth(SIENGE_USERNAME, SIENGE_PASSWORD)
56
- url = f"{SIENGE_BASE_URL}/{SIENGE_SUBDOMAIN}/public/api/v1{endpoint}"
230
+ url = f"{base_normalized}/v1{endpoint}"
57
231
  else:
58
232
  return {
59
233
  "success": False,
60
234
  "error": "No Authentication",
61
- "message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env"
235
+ "message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env",
236
+ "request_id": req_id
62
237
  }
63
-
238
+
64
239
  response = await client.request(
65
- method=method,
66
- url=url,
67
- headers=headers,
68
- params=params,
69
- json=json_data,
240
+ method=method,
241
+ url=url,
242
+ headers=headers,
243
+ params=normalized_params,
244
+ json=json_data,
70
245
  auth=auth
71
246
  )
72
247
 
248
+ latency = time.time() - start_time
249
+ _log_request(method, endpoint, response.status_code, latency, req_id)
250
+
73
251
  if response.status_code in [200, 201]:
74
252
  try:
75
- return {
76
- "success": True,
77
- "data": response.json(),
78
- "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)
79
260
  }
80
- except:
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:
81
270
  return {
82
- "success": True,
83
- "data": {"message": "Success"},
84
- "status_code": response.status_code
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)
85
276
  }
86
277
  else:
87
278
  return {
88
279
  "success": False,
89
280
  "error": f"HTTP {response.status_code}",
90
281
  "message": response.text,
91
- "status_code": response.status_code
282
+ "status_code": response.status_code,
283
+ "request_id": req_id,
284
+ "latency_ms": round(latency * 1000, 2)
92
285
  }
93
-
286
+
94
287
  except httpx.TimeoutException:
288
+ latency = time.time() - start_time
289
+ _log_request(method, endpoint, 408, latency, req_id)
95
290
  return {
96
- "success": False,
97
- "error": "Timeout",
98
- "message": f"A requisição excedeu o tempo limite de {REQUEST_TIMEOUT}s"
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)
99
296
  }
100
297
  except Exception as e:
298
+ latency = time.time() - start_time
299
+ _log_request(method, endpoint, 500, latency, req_id)
101
300
  return {
102
- "success": False,
103
- "error": str(e),
104
- "message": f"Erro na requisição: {str(e)}"
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)
105
306
  }
106
307
 
107
- async def make_sienge_bulk_request(method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None) -> Dict:
308
+
309
+ async def make_sienge_bulk_request(
310
+ method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None
311
+ ) -> Dict:
108
312
  """
109
313
  Função auxiliar para fazer requisições à API bulk-data do Sienge
110
314
  Suporta tanto Bearer Token quanto Basic Auth
315
+ MELHORADO: Observabilidade e normalização de parâmetros
111
316
  """
317
+ start_time = time.time()
318
+ req_id = str(uuid.uuid4())
319
+
112
320
  try:
321
+ # Normalizar parâmetros
322
+ normalized_params = to_query(params) if params else None
323
+
113
324
  async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
114
325
  headers = {
115
- "Content-Type": "application/json",
116
- "Accept": "application/json"
326
+ "Content-Type": "application/json",
327
+ "Accept": "application/json",
328
+ "X-Request-ID": req_id
117
329
  }
118
-
119
- # Configurar autenticação e URL para bulk-data
330
+
331
+ # Configurar autenticação e URL para bulk-data (corrigindo URLs duplas)
120
332
  auth = None
121
-
333
+ base_normalized = _normalize_url(SIENGE_BASE_URL, SIENGE_SUBDOMAIN)
334
+
122
335
  if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
123
- # Bearer Token (Recomendado)
124
336
  headers["Authorization"] = f"Bearer {SIENGE_API_KEY}"
125
- url = f"{SIENGE_BASE_URL}/{SIENGE_SUBDOMAIN}/public/api/bulk-data/v1{endpoint}"
337
+ url = f"{base_normalized}/bulk-data/v1{endpoint}"
126
338
  elif SIENGE_USERNAME and SIENGE_PASSWORD:
127
- # Basic Auth usando httpx.BasicAuth
128
339
  auth = httpx.BasicAuth(SIENGE_USERNAME, SIENGE_PASSWORD)
129
- url = f"{SIENGE_BASE_URL}/{SIENGE_SUBDOMAIN}/public/api/bulk-data/v1{endpoint}"
340
+ url = f"{base_normalized}/bulk-data/v1{endpoint}"
130
341
  else:
131
342
  return {
132
343
  "success": False,
133
344
  "error": "No Authentication",
134
- "message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env"
345
+ "message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env",
346
+ "request_id": req_id
135
347
  }
136
-
348
+
137
349
  response = await client.request(
138
- method=method,
139
- url=url,
140
- headers=headers,
141
- params=params,
142
- json=json_data,
350
+ method=method,
351
+ url=url,
352
+ headers=headers,
353
+ params=normalized_params,
354
+ json=json_data,
143
355
  auth=auth
144
356
  )
145
357
 
146
- if response.status_code in [200, 201]:
358
+ latency = time.time() - start_time
359
+ _log_request(method, f"bulk-data{endpoint}", response.status_code, latency, req_id)
360
+
361
+ if response.status_code in [200, 201, 202]:
147
362
  try:
148
363
  return {
149
- "success": True,
150
- "data": response.json(),
151
- "status_code": response.status_code
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)
152
369
  }
153
- except:
370
+ except Exception:
154
371
  return {
155
- "success": True,
156
- "data": {"message": "Success"},
157
- "status_code": response.status_code
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)
158
377
  }
159
378
  else:
160
379
  return {
161
380
  "success": False,
162
381
  "error": f"HTTP {response.status_code}",
163
382
  "message": response.text,
164
- "status_code": response.status_code
383
+ "status_code": response.status_code,
384
+ "request_id": req_id,
385
+ "latency_ms": round(latency * 1000, 2)
165
386
  }
166
-
387
+
167
388
  except httpx.TimeoutException:
389
+ latency = time.time() - start_time
390
+ _log_request(method, f"bulk-data{endpoint}", 408, latency, req_id)
168
391
  return {
169
- "success": False,
170
- "error": "Timeout",
171
- "message": f"A requisição excedeu o tempo limite de {REQUEST_TIMEOUT}s"
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)
172
397
  }
173
398
  except Exception as e:
399
+ latency = time.time() - start_time
400
+ _log_request(method, f"bulk-data{endpoint}", 500, latency, req_id)
174
401
  return {
175
- "success": False,
176
- "error": str(e),
177
- "message": f"Erro na requisição bulk-data: {str(e)}"
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)
178
407
  }
179
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)
657
+
658
+
180
659
  # ============ CONEXÃO E TESTE ============
181
660
 
661
+
182
662
  @mcp.tool
183
663
  async def test_sienge_connection() -> Dict:
184
664
  """Testa a conexão com a API do Sienge"""
185
665
  try:
186
- # Tentar endpoint mais simples primeiro
187
- result = await make_sienge_request("GET", "/customer-types")
188
-
666
+ # Usar serviço interno
667
+ result = await _svc_get_customer_types()
668
+
189
669
  if result["success"]:
190
670
  auth_method = "Bearer Token" if SIENGE_API_KEY else "Basic Auth"
191
671
  return {
@@ -193,7 +673,10 @@ async def test_sienge_connection() -> Dict:
193
673
  "message": "✅ Conexão com API do Sienge estabelecida com sucesso!",
194
674
  "api_status": "Online",
195
675
  "auth_method": auth_method,
196
- "timestamp": datetime.now().isoformat()
676
+ "timestamp": datetime.now().isoformat(),
677
+ "request_id": result.get("request_id"),
678
+ "latency_ms": result.get("latency_ms"),
679
+ "cache": result.get("cache")
197
680
  }
198
681
  else:
199
682
  return {
@@ -201,162 +684,391 @@ async def test_sienge_connection() -> Dict:
201
684
  "message": "❌ Falha ao conectar com API do Sienge",
202
685
  "error": result.get("error"),
203
686
  "details": result.get("message"),
204
- "timestamp": datetime.now().isoformat()
687
+ "timestamp": datetime.now().isoformat(),
688
+ "request_id": result.get("request_id")
205
689
  }
206
690
  except Exception as e:
207
691
  return {
208
692
  "success": False,
209
693
  "message": "❌ Erro ao testar conexão",
210
694
  "error": str(e),
211
- "timestamp": datetime.now().isoformat()
695
+ "timestamp": datetime.now().isoformat(),
212
696
  }
213
697
 
698
+
214
699
  # ============ CLIENTES ============
215
700
 
701
+
216
702
  @mcp.tool
217
- async def get_sienge_customers(limit: Optional[int] = 50, offset: Optional[int] = 0, search: Optional[str] = None, customer_type_id: Optional[str] = None) -> Dict:
703
+ 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
+ ) -> Dict:
218
706
  """
219
707
  Busca clientes no Sienge com filtros
220
-
708
+
221
709
  Args:
222
710
  limit: Máximo de registros (padrão: 50)
223
711
  offset: Pular registros (padrão: 0)
224
712
  search: Buscar por nome ou documento
225
713
  customer_type_id: Filtrar por tipo de cliente
226
714
  """
227
- params = {"limit": min(limit or 50, 200), "offset": offset or 0}
228
-
229
- if search:
230
- params["search"] = search
231
- if customer_type_id:
232
- params["customer_type_id"] = customer_type_id
233
-
234
- result = await make_sienge_request("GET", "/customers", params=params)
235
-
715
+ # Usar serviço interno
716
+ result = await _svc_get_customers(
717
+ limit=limit or 50,
718
+ offset=offset or 0,
719
+ search=search,
720
+ customer_type_id=customer_type_id
721
+ )
722
+
236
723
  if result["success"]:
237
724
  data = result["data"]
238
725
  customers = data.get("results", []) if isinstance(data, dict) else data
239
726
  metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
240
727
  total_count = metadata.get("count", len(customers))
241
-
728
+
242
729
  return {
243
730
  "success": True,
244
731
  "message": f"✅ Encontrados {len(customers)} clientes (total: {total_count})",
245
732
  "customers": customers,
246
733
  "count": len(customers),
247
- "filters_applied": params
734
+ "total_count": total_count,
735
+ "filters_applied": {
736
+ "limit": limit, "offset": offset, "search": search, "customer_type_id": customer_type_id
737
+ },
738
+ "request_id": result.get("request_id"),
739
+ "latency_ms": result.get("latency_ms"),
740
+ "cache": result.get("cache")
248
741
  }
249
-
742
+
250
743
  return {
251
744
  "success": False,
252
745
  "message": "❌ Erro ao buscar clientes",
253
746
  "error": result.get("error"),
254
- "details": result.get("message")
747
+ "details": result.get("message"),
748
+ "request_id": result.get("request_id")
255
749
  }
256
750
 
751
+
257
752
  @mcp.tool
258
753
  async def get_sienge_customer_types() -> Dict:
259
754
  """Lista tipos de clientes disponíveis"""
260
- result = await make_sienge_request("GET", "/customer-types")
261
-
755
+ # Usar serviço interno
756
+ result = await _svc_get_customer_types()
757
+
262
758
  if result["success"]:
263
759
  data = result["data"]
264
760
  customer_types = data.get("results", []) if isinstance(data, dict) else data
265
761
  metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
266
762
  total_count = metadata.get("count", len(customer_types))
267
-
763
+
268
764
  return {
269
765
  "success": True,
270
766
  "message": f"✅ Encontrados {len(customer_types)} tipos de clientes (total: {total_count})",
271
767
  "customer_types": customer_types,
272
- "count": len(customer_types)
768
+ "count": len(customer_types),
769
+ "total_count": total_count,
770
+ "request_id": result.get("request_id"),
771
+ "latency_ms": result.get("latency_ms"),
772
+ "cache": result.get("cache")
273
773
  }
274
-
774
+
275
775
  return {
276
776
  "success": False,
277
777
  "message": "❌ Erro ao buscar tipos de clientes",
278
778
  "error": result.get("error"),
279
- "details": result.get("message")
779
+ "details": result.get("message"),
780
+ "request_id": result.get("request_id")
280
781
  }
281
782
 
783
+
784
+ # ============ ALIAS COMPATÍVEIS COM CHECKLIST ============
785
+
786
+ @mcp.tool
787
+ async def get_sienge_enterprises(
788
+ limit: int = 100, offset: int = 0, company_id: int = None, enterprise_type: int = None
789
+ ) -> Dict:
790
+ """
791
+ ALIAS: get_sienge_projects → get_sienge_enterprises
792
+ Busca empreendimentos/obras (compatibilidade com checklist)
793
+ """
794
+ return await get_sienge_projects(
795
+ limit=limit,
796
+ offset=offset,
797
+ company_id=company_id,
798
+ enterprise_type=enterprise_type
799
+ )
800
+
801
+
802
+ @mcp.tool
803
+ async def get_sienge_suppliers(
804
+ limit: int = 50,
805
+ offset: int = 0,
806
+ search: str = None
807
+ ) -> Dict:
808
+ """
809
+ ALIAS: get_sienge_creditors → get_sienge_suppliers
810
+ Busca fornecedores (compatibilidade com checklist)
811
+ """
812
+ return await get_sienge_creditors(limit=limit, offset=offset, search=search)
813
+
814
+
815
+ @mcp.tool
816
+ async def search_sienge_finances(
817
+ period_start: str,
818
+ period_end: str,
819
+ account_type: Optional[str] = None,
820
+ cost_center: Optional[str] = None, # ignorado por enquanto (não suportado na API atual)
821
+ amount_filter: Optional[str] = None,
822
+ customer_creditor: Optional[str] = None
823
+ ) -> Dict:
824
+ """
825
+ ALIAS: search_sienge_financial_data → search_sienge_finances
826
+ - account_type: receivable | payable | both
827
+ - amount_filter: "100..500", ">=1000", "<=500", ">100", "<200", "=750"
828
+ - customer_creditor: termo de busca (cliente/credor)
829
+ """
830
+ # 1) mapear tipo
831
+ search_type = (account_type or "both").lower()
832
+ if search_type not in {"receivable", "payable", "both"}:
833
+ search_type = "both"
834
+
835
+ # 2) parse de faixa de valores
836
+ amount_min = amount_max = None
837
+ if amount_filter:
838
+ s = amount_filter.replace(" ", "")
839
+ try:
840
+ if ".." in s:
841
+ lo, hi = s.split("..", 1)
842
+ amount_min = float(lo) if lo else None
843
+ amount_max = float(hi) if hi else None
844
+ elif s.startswith(">="):
845
+ amount_min = float(s[2:])
846
+ elif s.startswith("<="):
847
+ amount_max = float(s[2:])
848
+ elif s.startswith(">"):
849
+ # >x → min = x (estrito não suportado; aproximamos)
850
+ amount_min = float(s[1:])
851
+ elif s.startswith("<"):
852
+ amount_max = float(s[1:])
853
+ elif s.startswith("="):
854
+ v = float(s[1:])
855
+ amount_min = v
856
+ amount_max = v
857
+ else:
858
+ # número puro → min
859
+ amount_min = float(s)
860
+ except ValueError:
861
+ # filtro inválido → ignora silenciosamente
862
+ amount_min = amount_max = None
863
+
864
+ return await search_sienge_financial_data(
865
+ period_start=period_start,
866
+ period_end=period_end,
867
+ search_type=search_type,
868
+ amount_min=amount_min,
869
+ amount_max=amount_max,
870
+ customer_creditor_search=customer_creditor
871
+ )
872
+
873
+
874
+ @mcp.tool
875
+ async def get_sienge_accounts_payable(
876
+ start_date: str = None, end_date: str = None, creditor_id: str = None,
877
+ status: str = None, limit: int = 50
878
+ ) -> Dict:
879
+ """
880
+ ALIAS: get_sienge_bills → get_sienge_accounts_payable
881
+ Busca contas a pagar (compatibilidade com checklist)
882
+ """
883
+ return await get_sienge_bills(
884
+ start_date=start_date,
885
+ end_date=end_date,
886
+ creditor_id=creditor_id,
887
+ status=status,
888
+ limit=limit
889
+ )
890
+
891
+
892
+ @mcp.tool
893
+ async def list_sienge_purchase_invoices(limit: int = 50, date_from: str = None) -> Dict:
894
+ """
895
+ Lista notas fiscais de compra (versão list/plural esperada pelo checklist)
896
+
897
+ Args:
898
+ limit: Máximo de registros (padrão: 50, máx: 200)
899
+ date_from: Data inicial (YYYY-MM-DD)
900
+ """
901
+ # Usar serviço interno
902
+ result = await _svc_get_purchase_invoices(limit=limit, date_from=date_from)
903
+
904
+ if result["success"]:
905
+ data = result["data"]
906
+ invoices = data.get("results", []) if isinstance(data, dict) else data
907
+ metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
908
+ total_count = metadata.get("count", len(invoices))
909
+
910
+ return {
911
+ "success": True,
912
+ "message": f"✅ Encontradas {len(invoices)} notas fiscais de compra (total: {total_count})",
913
+ "purchase_invoices": invoices,
914
+ "count": len(invoices),
915
+ "total_count": total_count,
916
+ "filters_applied": {"limit": limit, "date_from": date_from},
917
+ "request_id": result.get("request_id"),
918
+ "latency_ms": result.get("latency_ms"),
919
+ "cache": result.get("cache")
920
+ }
921
+
922
+ return {
923
+ "success": False,
924
+ "message": "❌ Erro ao buscar notas fiscais de compra",
925
+ "error": result.get("error"),
926
+ "details": result.get("message"),
927
+ "request_id": result.get("request_id")
928
+ }
929
+
930
+
931
+ @mcp.tool
932
+ async def list_sienge_purchase_requests(limit: int = 50, status: str = None) -> Dict:
933
+ """
934
+ Lista solicitações de compra (versão list/plural esperada pelo checklist)
935
+
936
+ Args:
937
+ limit: Máximo de registros (padrão: 50, máx: 200)
938
+ status: Status da solicitação
939
+ """
940
+ # Usar serviço interno
941
+ result = await _svc_get_purchase_requests(limit=limit, status=status)
942
+
943
+ if result["success"]:
944
+ data = result["data"]
945
+ requests = data.get("results", []) if isinstance(data, dict) else data
946
+ metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
947
+ total_count = metadata.get("count", len(requests))
948
+
949
+ return {
950
+ "success": True,
951
+ "message": f"✅ Encontradas {len(requests)} solicitações de compra (total: {total_count})",
952
+ "purchase_requests": requests,
953
+ "count": len(requests),
954
+ "total_count": total_count,
955
+ "filters_applied": {"limit": limit, "status": status},
956
+ "request_id": result.get("request_id"),
957
+ "latency_ms": result.get("latency_ms"),
958
+ "cache": result.get("cache")
959
+ }
960
+
961
+ return {
962
+ "success": False,
963
+ "message": "❌ Erro ao buscar solicitações de compra",
964
+ "error": result.get("error"),
965
+ "details": result.get("message"),
966
+ "request_id": result.get("request_id")
967
+ }
968
+
969
+
282
970
  # ============ CREDORES ============
283
971
 
972
+
284
973
  @mcp.tool
285
974
  async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int] = 0, search: Optional[str] = None) -> Dict:
286
975
  """
287
976
  Busca credores/fornecedores
288
-
977
+
289
978
  Args:
290
979
  limit: Máximo de registros (padrão: 50)
291
980
  offset: Pular registros (padrão: 0)
292
981
  search: Buscar por nome
293
982
  """
294
- params = {"limit": min(limit or 50, 200), "offset": offset or 0}
295
- if search:
296
- params["search"] = search
297
-
298
- result = await make_sienge_request("GET", "/creditors", params=params)
299
-
983
+ # Usar serviço interno
984
+ result = await _svc_get_creditors(
985
+ limit=limit or 50,
986
+ offset=offset or 0,
987
+ search=search
988
+ )
989
+
300
990
  if result["success"]:
301
991
  data = result["data"]
302
992
  creditors = data.get("results", []) if isinstance(data, dict) else data
303
993
  metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
304
994
  total_count = metadata.get("count", len(creditors))
305
-
995
+
306
996
  return {
307
997
  "success": True,
308
998
  "message": f"✅ Encontrados {len(creditors)} credores (total: {total_count})",
309
999
  "creditors": creditors,
310
- "count": len(creditors)
1000
+ "count": len(creditors),
1001
+ "total_count": total_count,
1002
+ "filters_applied": {"limit": limit, "offset": offset, "search": search},
1003
+ "request_id": result.get("request_id"),
1004
+ "latency_ms": result.get("latency_ms"),
1005
+ "cache": result.get("cache")
311
1006
  }
312
-
1007
+
313
1008
  return {
314
1009
  "success": False,
315
1010
  "message": "❌ Erro ao buscar credores",
316
1011
  "error": result.get("error"),
317
- "details": result.get("message")
1012
+ "details": result.get("message"),
1013
+ "request_id": result.get("request_id")
318
1014
  }
319
1015
 
1016
+
320
1017
  @mcp.tool
321
1018
  async def get_sienge_creditor_bank_info(creditor_id: str) -> Dict:
322
1019
  """
323
1020
  Consulta informações bancárias de um credor
324
-
1021
+
325
1022
  Args:
326
1023
  creditor_id: ID do credor (obrigatório)
327
1024
  """
328
- result = await make_sienge_request("GET", f"/creditors/{creditor_id}/bank-informations")
329
-
1025
+ # Usar serviço interno
1026
+ result = await _svc_get_creditor_bank_info(creditor_id=creditor_id)
1027
+
330
1028
  if result["success"]:
331
1029
  return {
332
1030
  "success": True,
333
1031
  "message": f"✅ Informações bancárias do credor {creditor_id}",
334
1032
  "creditor_id": creditor_id,
335
- "bank_info": result["data"]
1033
+ "bank_info": result["data"],
1034
+ "request_id": result.get("request_id"),
1035
+ "latency_ms": result.get("latency_ms")
336
1036
  }
337
-
1037
+
338
1038
  return {
339
1039
  "success": False,
340
1040
  "message": f"❌ Erro ao buscar info bancária do credor {creditor_id}",
341
1041
  "error": result.get("error"),
342
- "details": result.get("message")
1042
+ "details": result.get("message"),
1043
+ "request_id": result.get("request_id")
343
1044
  }
344
1045
 
1046
+
345
1047
  # ============ FINANCEIRO ============
346
1048
 
1049
+
347
1050
  @mcp.tool
348
- async def get_sienge_accounts_receivable(start_date: str, end_date: str, selection_type: str = "D",
349
- company_id: Optional[int] = None, cost_centers_id: Optional[List[int]] = None,
350
- correction_indexer_id: Optional[int] = None, correction_date: Optional[str] = None,
351
- change_start_date: Optional[str] = None, completed_bills: Optional[str] = None,
352
- origins_ids: Optional[List[str]] = None, bearers_id_in: Optional[List[int]] = None,
353
- bearers_id_not_in: Optional[List[int]] = None) -> Dict:
1051
+ async def get_sienge_accounts_receivable(
1052
+ start_date: str,
1053
+ end_date: str,
1054
+ selection_type: str = "D",
1055
+ company_id: Optional[int] = None,
1056
+ cost_centers_id: Optional[List[int]] = None,
1057
+ correction_indexer_id: Optional[int] = None,
1058
+ correction_date: Optional[str] = None,
1059
+ change_start_date: Optional[str] = None,
1060
+ completed_bills: Optional[str] = None,
1061
+ origins_ids: Optional[List[str]] = None,
1062
+ bearers_id_in: Optional[List[int]] = None,
1063
+ bearers_id_not_in: Optional[List[int]] = None,
1064
+ ) -> Dict:
354
1065
  """
355
1066
  Consulta parcelas do contas a receber via API bulk-data
356
-
1067
+ MELHORADO: Suporte a polling assíncrono para requests 202
1068
+
357
1069
  Args:
358
1070
  start_date: Data de início do período (YYYY-MM-DD) - OBRIGATÓRIO
359
- end_date: Data do fim do período (YYYY-MM-DD) - OBRIGATÓRIO
1071
+ end_date: Data do fim do período (YYYY-MM-DD) - OBRIGATÓRIO
360
1072
  selection_type: Seleção da data do período (I=emissão, D=vencimento, P=pagamento, B=competência) - padrão: D
361
1073
  company_id: Código da empresa
362
1074
  cost_centers_id: Lista de códigos de centro de custo
@@ -368,103 +1080,135 @@ async def get_sienge_accounts_receivable(start_date: str, end_date: str, selecti
368
1080
  bearers_id_in: Filtrar parcelas com códigos de portador específicos
369
1081
  bearers_id_not_in: Filtrar parcelas excluindo códigos de portador específicos
370
1082
  """
371
- params = {
372
- "startDate": start_date,
373
- "endDate": end_date,
374
- "selectionType": selection_type
375
- }
376
-
377
- if company_id:
378
- params["companyId"] = company_id
379
- if cost_centers_id:
380
- params["costCentersId"] = cost_centers_id
381
- if correction_indexer_id:
382
- params["correctionIndexerId"] = correction_indexer_id
383
- if correction_date:
384
- params["correctionDate"] = correction_date
385
- if change_start_date:
386
- params["changeStartDate"] = change_start_date
387
- if completed_bills:
388
- params["completedBills"] = completed_bills
389
- if origins_ids:
390
- params["originsIds"] = origins_ids
391
- if bearers_id_in:
392
- params["bearersIdIn"] = bearers_id_in
393
- if bearers_id_not_in:
394
- params["bearersIdNotIn"] = bearers_id_not_in
395
-
396
- result = await make_sienge_bulk_request("GET", "/income", params=params)
397
-
1083
+ # Usar serviço interno com polling assíncrono
1084
+ result = await _svc_get_accounts_receivable(
1085
+ start_date=start_date,
1086
+ end_date=end_date,
1087
+ selection_type=selection_type,
1088
+ company_id=company_id,
1089
+ cost_centers_id=cost_centers_id,
1090
+ correction_indexer_id=correction_indexer_id,
1091
+ correction_date=correction_date,
1092
+ change_start_date=change_start_date,
1093
+ completed_bills=completed_bills,
1094
+ origins_ids=origins_ids,
1095
+ bearers_id_in=bearers_id_in,
1096
+ bearers_id_not_in=bearers_id_not_in
1097
+ )
1098
+
398
1099
  if result["success"]:
399
- data = result["data"]
400
- income_data = data.get("data", []) if isinstance(data, dict) else data
1100
+ # Para requests normais (200) e assíncronos processados
1101
+ income_data = result.get("data", [])
401
1102
 
402
- return {
1103
+ response = {
403
1104
  "success": True,
404
1105
  "message": f"✅ Encontradas {len(income_data)} parcelas a receber",
405
1106
  "income_data": income_data,
406
1107
  "count": len(income_data),
407
1108
  "period": f"{start_date} a {end_date}",
408
1109
  "selection_type": selection_type,
409
- "filters": params
1110
+ "request_id": result.get("request_id"),
1111
+ "latency_ms": result.get("latency_ms")
410
1112
  }
411
-
1113
+
1114
+ # Se foi processamento assíncrono, incluir informações extras
1115
+ if result.get("async_identifier"):
1116
+ response.update({
1117
+ "async_processing": {
1118
+ "identifier": result.get("async_identifier"),
1119
+ "correlation_id": result.get("correlation_id"),
1120
+ "chunks_downloaded": result.get("chunks_downloaded"),
1121
+ "rows_returned": result.get("rows_returned"),
1122
+ "polling_attempts": result.get("polling_attempts")
1123
+ }
1124
+ })
1125
+
1126
+ return response
1127
+
412
1128
  return {
413
1129
  "success": False,
414
1130
  "message": "❌ Erro ao buscar parcelas a receber",
415
1131
  "error": result.get("error"),
416
- "details": result.get("message")
1132
+ "details": result.get("message"),
1133
+ "request_id": result.get("request_id"),
1134
+ "async_info": {
1135
+ "identifier": result.get("async_identifier"),
1136
+ "polling_attempts": result.get("polling_attempts")
1137
+ } if result.get("async_identifier") else None
417
1138
  }
418
1139
 
1140
+
419
1141
  @mcp.tool
420
- async def get_sienge_accounts_receivable_by_bills(bills_ids: List[int], correction_indexer_id: Optional[int] = None,
421
- correction_date: Optional[str] = None) -> Dict:
1142
+ async def get_sienge_accounts_receivable_by_bills(
1143
+ bills_ids: List[int], correction_indexer_id: Optional[int] = None, correction_date: Optional[str] = None
1144
+ ) -> Dict:
422
1145
  """
423
1146
  Consulta parcelas dos títulos informados via API bulk-data
424
-
1147
+ MELHORADO: Suporte a polling assíncrono para requests 202
1148
+
425
1149
  Args:
426
1150
  bills_ids: Lista de códigos dos títulos - OBRIGATÓRIO
427
1151
  correction_indexer_id: Código do indexador de correção
428
1152
  correction_date: Data para correção do indexador (YYYY-MM-DD)
429
1153
  """
430
- params = {
431
- "billsIds": bills_ids
432
- }
433
-
434
- if correction_indexer_id:
435
- params["correctionIndexerId"] = correction_indexer_id
436
- if correction_date:
437
- params["correctionDate"] = correction_date
438
-
439
- result = await make_sienge_bulk_request("GET", "/income/by-bills", params=params)
440
-
1154
+ # Usar serviço interno com polling assíncrono
1155
+ result = await _svc_get_accounts_receivable_by_bills(
1156
+ bills_ids=bills_ids,
1157
+ correction_indexer_id=correction_indexer_id,
1158
+ correction_date=correction_date
1159
+ )
1160
+
441
1161
  if result["success"]:
442
- data = result["data"]
443
- income_data = data.get("data", []) if isinstance(data, dict) else data
1162
+ income_data = result.get("data", [])
444
1163
 
445
- return {
1164
+ response = {
446
1165
  "success": True,
447
1166
  "message": f"✅ Encontradas {len(income_data)} parcelas dos títulos informados",
448
1167
  "income_data": income_data,
449
1168
  "count": len(income_data),
450
1169
  "bills_consulted": bills_ids,
451
- "filters": params
1170
+ "request_id": result.get("request_id"),
1171
+ "latency_ms": result.get("latency_ms")
452
1172
  }
453
-
1173
+
1174
+ # Se foi processamento assíncrono, incluir informações extras
1175
+ if result.get("async_identifier"):
1176
+ response.update({
1177
+ "async_processing": {
1178
+ "identifier": result.get("async_identifier"),
1179
+ "correlation_id": result.get("correlation_id"),
1180
+ "chunks_downloaded": result.get("chunks_downloaded"),
1181
+ "rows_returned": result.get("rows_returned"),
1182
+ "polling_attempts": result.get("polling_attempts")
1183
+ }
1184
+ })
1185
+
1186
+ return response
1187
+
454
1188
  return {
455
1189
  "success": False,
456
1190
  "message": "❌ Erro ao buscar parcelas dos títulos informados",
457
1191
  "error": result.get("error"),
458
- "details": result.get("message")
1192
+ "details": result.get("message"),
1193
+ "request_id": result.get("request_id"),
1194
+ "async_info": {
1195
+ "identifier": result.get("async_identifier"),
1196
+ "polling_attempts": result.get("polling_attempts")
1197
+ } if result.get("async_identifier") else None
459
1198
  }
460
1199
 
1200
+
461
1201
  @mcp.tool
462
- async def get_sienge_bills(start_date: Optional[str] = None, end_date: Optional[str] = None,
463
- creditor_id: Optional[str] = None, status: Optional[str] = None,
464
- limit: Optional[int] = 50) -> Dict:
1202
+ async def get_sienge_bills(
1203
+ start_date: Optional[str] = None,
1204
+ end_date: Optional[str] = None,
1205
+ creditor_id: Optional[str] = None,
1206
+ status: Optional[str] = None,
1207
+ limit: Optional[int] = 50,
1208
+ ) -> Dict:
465
1209
  """
466
1210
  Consulta títulos a pagar (contas a pagar) - REQUER startDate obrigatório
467
-
1211
+
468
1212
  Args:
469
1213
  start_date: Data inicial obrigatória (YYYY-MM-DD) - padrão últimos 30 dias
470
1214
  end_date: Data final (YYYY-MM-DD) - padrão hoje
@@ -472,62 +1216,72 @@ async def get_sienge_bills(start_date: Optional[str] = None, end_date: Optional[
472
1216
  status: Status do título (ex: open, paid, cancelled)
473
1217
  limit: Máximo de registros (padrão: 50, máx: 200)
474
1218
  """
475
- from datetime import datetime, timedelta
476
-
477
- # Se start_date não fornecido, usar últimos 30 dias
478
- if not start_date:
479
- start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
480
-
481
- # Se end_date não fornecido, usar hoje
482
- if not end_date:
483
- end_date = datetime.now().strftime('%Y-%m-%d')
484
-
485
- # Parâmetros obrigatórios
486
- params = {
487
- "startDate": start_date, # OBRIGATÓRIO pela API
488
- "endDate": end_date,
489
- "limit": min(limit or 50, 200)
490
- }
491
-
492
- # Parâmetros opcionais
493
- if creditor_id:
494
- params["creditor_id"] = creditor_id
495
- if status:
496
- params["status"] = status
497
-
498
- result = await make_sienge_request("GET", "/bills", params=params)
499
-
1219
+ # Usar serviço interno
1220
+ result = await _svc_get_bills(
1221
+ start_date=start_date,
1222
+ end_date=end_date,
1223
+ creditor_id=creditor_id,
1224
+ status=status,
1225
+ limit=limit or 50
1226
+ )
1227
+
500
1228
  if result["success"]:
501
1229
  data = result["data"]
502
1230
  bills = data.get("results", []) if isinstance(data, dict) else data
503
1231
  metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
504
1232
  total_count = metadata.get("count", len(bills))
505
-
1233
+
1234
+ # Aplicar parsing numérico nos valores
1235
+ for bill in bills:
1236
+ if "amount" in bill:
1237
+ bill["amount"] = _parse_numeric_value(bill["amount"])
1238
+ if "paid_amount" in bill:
1239
+ bill["paid_amount"] = _parse_numeric_value(bill["paid_amount"])
1240
+ if "remaining_amount" in bill:
1241
+ bill["remaining_amount"] = _parse_numeric_value(bill["remaining_amount"])
1242
+
1243
+ # Usar datas padrão se não fornecidas
1244
+ actual_start = start_date or (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
1245
+ actual_end = end_date or datetime.now().strftime("%Y-%m-%d")
1246
+
506
1247
  return {
507
1248
  "success": True,
508
- "message": f"✅ Encontrados {len(bills)} títulos a pagar (total: {total_count}) - período: {start_date} a {end_date}",
1249
+ "message": f"✅ Encontrados {len(bills)} títulos a pagar (total: {total_count}) - período: {actual_start} a {actual_end}",
509
1250
  "bills": bills,
510
1251
  "count": len(bills),
511
1252
  "total_count": total_count,
512
- "period": {"start_date": start_date, "end_date": end_date},
513
- "filters": params
1253
+ "period": {"start_date": actual_start, "end_date": actual_end},
1254
+ "filters_applied": {
1255
+ "start_date": actual_start, "end_date": actual_end,
1256
+ "creditor_id": creditor_id, "status": status, "limit": limit
1257
+ },
1258
+ "request_id": result.get("request_id"),
1259
+ "latency_ms": result.get("latency_ms"),
1260
+ "cache": result.get("cache")
514
1261
  }
515
-
1262
+
516
1263
  return {
517
1264
  "success": False,
518
1265
  "message": "❌ Erro ao buscar títulos a pagar",
519
1266
  "error": result.get("error"),
520
- "details": result.get("message")
1267
+ "details": result.get("message"),
1268
+ "request_id": result.get("request_id")
521
1269
  }
522
1270
 
1271
+
523
1272
  # ============ COMPRAS ============
524
1273
 
1274
+
525
1275
  @mcp.tool
526
- async def get_sienge_purchase_orders(purchase_order_id: Optional[str] = None, status: Optional[str] = None,
527
- date_from: Optional[str] = None, limit: Optional[int] = 50) -> Dict:
1276
+ async def get_sienge_purchase_orders(
1277
+ purchase_order_id: Optional[str] = None,
1278
+ status: Optional[str] = None,
1279
+ date_from: Optional[str] = None,
1280
+ limit: Optional[int] = 50,
1281
+ ) -> Dict:
528
1282
  """
529
1283
  Consulta pedidos de compra
530
-
1284
+
531
1285
  Args:
532
1286
  purchase_order_id: ID específico do pedido
533
1287
  status: Status do pedido
@@ -537,73 +1291,75 @@ async def get_sienge_purchase_orders(purchase_order_id: Optional[str] = None, st
537
1291
  if purchase_order_id:
538
1292
  result = await make_sienge_request("GET", f"/purchase-orders/{purchase_order_id}")
539
1293
  if result["success"]:
540
- return {
541
- "success": True,
542
- "message": f"✅ Pedido {purchase_order_id} encontrado",
543
- "purchase_order": result["data"]
544
- }
1294
+ return {"success": True, "message": f"✅ Pedido {purchase_order_id} encontrado", "purchase_order": result["data"]}
545
1295
  return result
546
-
1296
+
547
1297
  params = {"limit": min(limit or 50, 200)}
548
1298
  if status:
549
1299
  params["status"] = status
550
1300
  if date_from:
551
1301
  params["date_from"] = date_from
552
-
1302
+
553
1303
  result = await make_sienge_request("GET", "/purchase-orders", params=params)
554
-
1304
+
555
1305
  if result["success"]:
556
1306
  data = result["data"]
557
1307
  orders = data.get("results", []) if isinstance(data, dict) else data
558
-
1308
+
559
1309
  return {
560
1310
  "success": True,
561
1311
  "message": f"✅ Encontrados {len(orders)} pedidos de compra",
562
1312
  "purchase_orders": orders,
563
- "count": len(orders)
1313
+ "count": len(orders),
1314
+ "request_id": result.get("request_id"),
1315
+ "latency_ms": result.get("latency_ms")
564
1316
  }
565
-
1317
+
566
1318
  return {
567
1319
  "success": False,
568
1320
  "message": "❌ Erro ao buscar pedidos de compra",
569
1321
  "error": result.get("error"),
570
- "details": result.get("message")
1322
+ "details": result.get("message"),
1323
+ "request_id": result.get("request_id"),
1324
+ "latency_ms": result.get("latency_ms")
571
1325
  }
572
1326
 
1327
+
573
1328
  @mcp.tool
574
1329
  async def get_sienge_purchase_order_items(purchase_order_id: str) -> Dict:
575
1330
  """
576
1331
  Consulta itens de um pedido de compra específico
577
-
1332
+
578
1333
  Args:
579
1334
  purchase_order_id: ID do pedido (obrigatório)
580
1335
  """
581
1336
  result = await make_sienge_request("GET", f"/purchase-orders/{purchase_order_id}/items")
582
-
1337
+
583
1338
  if result["success"]:
584
1339
  data = result["data"]
585
1340
  items = data.get("results", []) if isinstance(data, dict) else data
586
-
1341
+
587
1342
  return {
588
1343
  "success": True,
589
1344
  "message": f"✅ Encontrados {len(items)} itens no pedido {purchase_order_id}",
590
1345
  "purchase_order_id": purchase_order_id,
591
1346
  "items": items,
592
- "count": len(items)
1347
+ "count": len(items),
593
1348
  }
594
-
1349
+
595
1350
  return {
596
1351
  "success": False,
597
1352
  "message": f"❌ Erro ao buscar itens do pedido {purchase_order_id}",
598
1353
  "error": result.get("error"),
599
- "details": result.get("message")
1354
+ "details": result.get("message"),
600
1355
  }
601
1356
 
1357
+
602
1358
  @mcp.tool
603
1359
  async def get_sienge_purchase_requests(purchase_request_id: Optional[str] = None, limit: Optional[int] = 50) -> Dict:
604
1360
  """
605
1361
  Consulta solicitações de compra
606
-
1362
+
607
1363
  Args:
608
1364
  purchase_request_id: ID específico da solicitação
609
1365
  limit: Máximo de registros
@@ -614,36 +1370,37 @@ async def get_sienge_purchase_requests(purchase_request_id: Optional[str] = None
614
1370
  return {
615
1371
  "success": True,
616
1372
  "message": f"✅ Solicitação {purchase_request_id} encontrada",
617
- "purchase_request": result["data"]
1373
+ "purchase_request": result["data"],
618
1374
  }
619
1375
  return result
620
-
1376
+
621
1377
  params = {"limit": min(limit or 50, 200)}
622
1378
  result = await make_sienge_request("GET", "/purchase-requests", params=params)
623
-
1379
+
624
1380
  if result["success"]:
625
1381
  data = result["data"]
626
1382
  requests = data.get("results", []) if isinstance(data, dict) else data
627
-
1383
+
628
1384
  return {
629
1385
  "success": True,
630
1386
  "message": f"✅ Encontradas {len(requests)} solicitações de compra",
631
1387
  "purchase_requests": requests,
632
- "count": len(requests)
1388
+ "count": len(requests),
633
1389
  }
634
-
1390
+
635
1391
  return {
636
1392
  "success": False,
637
1393
  "message": "❌ Erro ao buscar solicitações de compra",
638
1394
  "error": result.get("error"),
639
- "details": result.get("message")
1395
+ "details": result.get("message"),
640
1396
  }
641
1397
 
1398
+
642
1399
  @mcp.tool
643
1400
  async def create_sienge_purchase_request(description: str, project_id: str, items: List[Dict[str, Any]]) -> Dict:
644
1401
  """
645
1402
  Cria nova solicitação de compra
646
-
1403
+
647
1404
  Args:
648
1405
  description: Descrição da solicitação
649
1406
  project_id: ID do projeto/obra
@@ -653,89 +1410,102 @@ async def create_sienge_purchase_request(description: str, project_id: str, item
653
1410
  "description": description,
654
1411
  "project_id": project_id,
655
1412
  "items": items,
656
- "date": datetime.now().strftime("%Y-%m-%d")
1413
+ "date": datetime.now().strftime("%Y-%m-%d"),
657
1414
  }
658
-
659
- result = await make_sienge_request("POST", "/purchase-requests", json_data=request_data)
660
-
1415
+
1416
+ # CORRIGIDO: Normalizar JSON payload
1417
+ json_data = to_camel_json(request_data)
1418
+ result = await make_sienge_request("POST", "/purchase-requests", json_data=json_data)
1419
+
661
1420
  if result["success"]:
662
1421
  return {
663
1422
  "success": True,
664
1423
  "message": "✅ Solicitação de compra criada com sucesso",
665
- "request_id": result["data"].get("id"),
666
- "data": result["data"]
1424
+ "request_id": result.get("request_id"),
1425
+ "purchase_request_id": result["data"].get("id"),
1426
+ "data": result["data"],
1427
+ "latency_ms": result.get("latency_ms")
667
1428
  }
668
-
1429
+
669
1430
  return {
670
1431
  "success": False,
671
1432
  "message": "❌ Erro ao criar solicitação de compra",
672
1433
  "error": result.get("error"),
673
- "details": result.get("message")
1434
+ "details": result.get("message"),
1435
+ "request_id": result.get("request_id")
674
1436
  }
675
1437
 
1438
+
676
1439
  # ============ NOTAS FISCAIS DE COMPRA ============
677
1440
 
1441
+
678
1442
  @mcp.tool
679
1443
  async def get_sienge_purchase_invoice(sequential_number: int) -> Dict:
680
1444
  """
681
1445
  Consulta nota fiscal de compra por número sequencial
682
-
1446
+
683
1447
  Args:
684
1448
  sequential_number: Número sequencial da nota fiscal
685
1449
  """
686
1450
  result = await make_sienge_request("GET", f"/purchase-invoices/{sequential_number}")
687
-
1451
+
688
1452
  if result["success"]:
689
- return {
690
- "success": True,
691
- "message": f"✅ Nota fiscal {sequential_number} encontrada",
692
- "invoice": result["data"]
693
- }
694
-
1453
+ return {"success": True, "message": f"✅ Nota fiscal {sequential_number} encontrada", "invoice": result["data"]}
1454
+
695
1455
  return {
696
1456
  "success": False,
697
1457
  "message": f"❌ Erro ao buscar nota fiscal {sequential_number}",
698
1458
  "error": result.get("error"),
699
- "details": result.get("message")
1459
+ "details": result.get("message"),
700
1460
  }
701
1461
 
1462
+
702
1463
  @mcp.tool
703
1464
  async def get_sienge_purchase_invoice_items(sequential_number: int) -> Dict:
704
1465
  """
705
1466
  Consulta itens de uma nota fiscal de compra
706
-
1467
+
707
1468
  Args:
708
1469
  sequential_number: Número sequencial da nota fiscal
709
1470
  """
710
1471
  result = await make_sienge_request("GET", f"/purchase-invoices/{sequential_number}/items")
711
-
1472
+
712
1473
  if result["success"]:
713
1474
  data = result["data"]
714
1475
  items = data.get("results", []) if isinstance(data, dict) else data
715
1476
  metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
716
-
1477
+
717
1478
  return {
718
1479
  "success": True,
719
1480
  "message": f"✅ Encontrados {len(items)} itens na nota fiscal {sequential_number}",
720
1481
  "items": items,
721
1482
  "count": len(items),
722
- "metadata": metadata
1483
+ "metadata": metadata,
723
1484
  }
724
-
1485
+
725
1486
  return {
726
1487
  "success": False,
727
1488
  "message": f"❌ Erro ao buscar itens da nota fiscal {sequential_number}",
728
1489
  "error": result.get("error"),
729
- "details": result.get("message")
1490
+ "details": result.get("message"),
730
1491
  }
731
1492
 
1493
+
732
1494
  @mcp.tool
733
- async def create_sienge_purchase_invoice(document_id: str, number: str, supplier_id: int, company_id: int,
734
- movement_type_id: int, movement_date: str, issue_date: str,
735
- series: Optional[str] = None, notes: Optional[str] = None) -> Dict:
1495
+ async def create_sienge_purchase_invoice(
1496
+ document_id: str,
1497
+ number: str,
1498
+ supplier_id: int,
1499
+ company_id: int,
1500
+ movement_type_id: int,
1501
+ movement_date: str,
1502
+ issue_date: str,
1503
+ series: Optional[str] = None,
1504
+ notes: Optional[str] = None,
1505
+ ) -> Dict:
736
1506
  """
737
1507
  Cadastra uma nova nota fiscal de compra
738
-
1508
+
739
1509
  Args:
740
1510
  document_id: ID do documento (ex: "NF")
741
1511
  number: Número da nota fiscal
@@ -748,43 +1518,44 @@ async def create_sienge_purchase_invoice(document_id: str, number: str, supplier
748
1518
  notes: Observações (opcional)
749
1519
  """
750
1520
  invoice_data = {
751
- "documentId": document_id,
1521
+ "document_id": document_id,
752
1522
  "number": number,
753
- "supplierId": supplier_id,
754
- "companyId": company_id,
755
- "movementTypeId": movement_type_id,
756
- "movementDate": movement_date,
757
- "issueDate": issue_date
1523
+ "supplier_id": supplier_id,
1524
+ "company_id": company_id,
1525
+ "movement_type_id": movement_type_id,
1526
+ "movement_date": movement_date,
1527
+ "issue_date": issue_date,
758
1528
  }
759
-
1529
+
760
1530
  if series:
761
1531
  invoice_data["series"] = series
762
1532
  if notes:
763
1533
  invoice_data["notes"] = notes
764
-
765
- result = await make_sienge_request("POST", "/purchase-invoices", json_data=invoice_data)
766
-
1534
+
1535
+ result = await make_sienge_request("POST", "/purchase-invoices", json_data=to_camel_json(invoice_data))
1536
+
767
1537
  if result["success"]:
768
- return {
769
- "success": True,
770
- "message": f"✅ Nota fiscal {number} criada com sucesso",
771
- "invoice": result["data"]
772
- }
773
-
1538
+ return {"success": True, "message": f"✅ Nota fiscal {number} criada com sucesso", "invoice": result["data"]}
1539
+
774
1540
  return {
775
1541
  "success": False,
776
1542
  "message": f"❌ Erro ao criar nota fiscal {number}",
777
1543
  "error": result.get("error"),
778
- "details": result.get("message")
1544
+ "details": result.get("message"),
779
1545
  }
780
1546
 
1547
+
781
1548
  @mcp.tool
782
- async def add_items_to_purchase_invoice(sequential_number: int, deliveries_order: List[Dict[str, Any]],
783
- copy_notes_purchase_orders: bool = True, copy_notes_resources: bool = False,
784
- copy_attachments_purchase_orders: bool = True) -> Dict:
1549
+ async def add_items_to_purchase_invoice(
1550
+ sequential_number: int,
1551
+ deliveries_order: List[Dict[str, Any]],
1552
+ copy_notes_purchase_orders: bool = True,
1553
+ copy_notes_resources: bool = False,
1554
+ copy_attachments_purchase_orders: bool = True,
1555
+ ) -> Dict:
785
1556
  """
786
1557
  Insere itens em uma nota fiscal a partir de entregas de pedidos de compra
787
-
1558
+
788
1559
  Args:
789
1560
  sequential_number: Número sequencial da nota fiscal
790
1561
  deliveries_order: Lista de entregas com estrutura:
@@ -798,36 +1569,44 @@ async def add_items_to_purchase_invoice(sequential_number: int, deliveries_order
798
1569
  copy_attachments_purchase_orders: Copiar anexos dos pedidos de compra
799
1570
  """
800
1571
  item_data = {
801
- "deliveriesOrder": deliveries_order,
802
- "copyNotesPurchaseOrders": copy_notes_purchase_orders,
803
- "copyNotesResources": copy_notes_resources,
804
- "copyAttachmentsPurchaseOrders": copy_attachments_purchase_orders
1572
+ "deliveries_order": deliveries_order,
1573
+ "copy_notes_purchase_orders": copy_notes_purchase_orders,
1574
+ "copy_notes_resources": copy_notes_resources,
1575
+ "copy_attachments_purchase_orders": copy_attachments_purchase_orders,
805
1576
  }
806
-
807
- result = await make_sienge_request("POST", f"/purchase-invoices/{sequential_number}/items/purchase-orders/delivery-schedules", json_data=item_data)
808
-
1577
+
1578
+ result = await make_sienge_request(
1579
+ "POST", f"/purchase-invoices/{sequential_number}/items/purchase-orders/delivery-schedules", json_data=to_camel_json(item_data)
1580
+ )
1581
+
809
1582
  if result["success"]:
810
1583
  return {
811
1584
  "success": True,
812
1585
  "message": f"✅ Itens adicionados à nota fiscal {sequential_number} com sucesso",
813
- "item": result["data"]
1586
+ "item": result["data"],
814
1587
  }
815
-
1588
+
816
1589
  return {
817
1590
  "success": False,
818
1591
  "message": f"❌ Erro ao adicionar itens à nota fiscal {sequential_number}",
819
1592
  "error": result.get("error"),
820
- "details": result.get("message")
1593
+ "details": result.get("message"),
821
1594
  }
822
1595
 
1596
+
823
1597
  @mcp.tool
824
- async def get_sienge_purchase_invoices_deliveries_attended(bill_id: Optional[int] = None, sequential_number: Optional[int] = None,
825
- purchase_order_id: Optional[int] = None, invoice_item_number: Optional[int] = None,
826
- purchase_order_item_number: Optional[int] = None,
827
- limit: Optional[int] = 100, offset: Optional[int] = 0) -> Dict:
1598
+ async def get_sienge_purchase_invoices_deliveries_attended(
1599
+ bill_id: Optional[int] = None,
1600
+ sequential_number: Optional[int] = None,
1601
+ purchase_order_id: Optional[int] = None,
1602
+ invoice_item_number: Optional[int] = None,
1603
+ purchase_order_item_number: Optional[int] = None,
1604
+ limit: Optional[int] = 100,
1605
+ offset: Optional[int] = 0,
1606
+ ) -> Dict:
828
1607
  """
829
1608
  Lista entregas atendidas entre pedidos de compra e notas fiscais
830
-
1609
+
831
1610
  Args:
832
1611
  bill_id: ID do título da nota fiscal
833
1612
  sequential_number: Número sequencial da nota fiscal
@@ -838,7 +1617,7 @@ async def get_sienge_purchase_invoices_deliveries_attended(bill_id: Optional[int
838
1617
  offset: Deslocamento (padrão: 0)
839
1618
  """
840
1619
  params = {"limit": min(limit or 100, 200), "offset": offset or 0}
841
-
1620
+
842
1621
  if bill_id:
843
1622
  params["billId"] = bill_id
844
1623
  if sequential_number:
@@ -849,37 +1628,39 @@ async def get_sienge_purchase_invoices_deliveries_attended(bill_id: Optional[int
849
1628
  params["invoiceItemNumber"] = invoice_item_number
850
1629
  if purchase_order_item_number:
851
1630
  params["purchaseOrderItemNumber"] = purchase_order_item_number
852
-
1631
+
853
1632
  result = await make_sienge_request("GET", "/purchase-invoices/deliveries-attended", params=params)
854
-
1633
+
855
1634
  if result["success"]:
856
1635
  data = result["data"]
857
1636
  deliveries = data.get("results", []) if isinstance(data, dict) else data
858
1637
  metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
859
-
1638
+
860
1639
  return {
861
1640
  "success": True,
862
1641
  "message": f"✅ Encontradas {len(deliveries)} entregas atendidas",
863
1642
  "deliveries": deliveries,
864
1643
  "count": len(deliveries),
865
1644
  "metadata": metadata,
866
- "filters": params
1645
+ "filters": params,
867
1646
  }
868
-
1647
+
869
1648
  return {
870
1649
  "success": False,
871
1650
  "message": "❌ Erro ao buscar entregas atendidas",
872
1651
  "error": result.get("error"),
873
- "details": result.get("message")
1652
+ "details": result.get("message"),
874
1653
  }
875
1654
 
1655
+
876
1656
  # ============ ESTOQUE ============
877
1657
 
1658
+
878
1659
  @mcp.tool
879
1660
  async def get_sienge_stock_inventory(cost_center_id: str, resource_id: Optional[str] = None) -> Dict:
880
1661
  """
881
1662
  Consulta inventário de estoque por centro de custo
882
-
1663
+
883
1664
  Args:
884
1665
  cost_center_id: ID do centro de custo (obrigatório)
885
1666
  resource_id: ID do insumo específico (opcional)
@@ -888,67 +1669,76 @@ async def get_sienge_stock_inventory(cost_center_id: str, resource_id: Optional[
888
1669
  endpoint = f"/stock-inventories/{cost_center_id}/items/{resource_id}"
889
1670
  else:
890
1671
  endpoint = f"/stock-inventories/{cost_center_id}/items"
891
-
1672
+
892
1673
  result = await make_sienge_request("GET", endpoint)
893
-
1674
+
894
1675
  if result["success"]:
895
1676
  data = result["data"]
896
1677
  items = data.get("results", []) if isinstance(data, dict) else data
897
1678
  count = len(items) if isinstance(items, list) else 1
898
-
1679
+
899
1680
  return {
900
1681
  "success": True,
901
1682
  "message": f"✅ Inventário do centro de custo {cost_center_id}",
902
1683
  "cost_center_id": cost_center_id,
903
1684
  "inventory": items,
904
- "count": count
1685
+ "count": count,
905
1686
  }
906
-
1687
+
907
1688
  return {
908
1689
  "success": False,
909
1690
  "message": f"❌ Erro ao consultar estoque do centro {cost_center_id}",
910
1691
  "error": result.get("error"),
911
- "details": result.get("message")
1692
+ "details": result.get("message"),
912
1693
  }
913
1694
 
1695
+
914
1696
  @mcp.tool
915
1697
  async def get_sienge_stock_reservations(limit: Optional[int] = 50) -> Dict:
916
1698
  """
917
1699
  Lista reservas de estoque
918
-
1700
+
919
1701
  Args:
920
1702
  limit: Máximo de registros
921
1703
  """
922
1704
  params = {"limit": min(limit or 50, 200)}
923
1705
  result = await make_sienge_request("GET", "/stock-reservations", params=params)
924
-
1706
+
925
1707
  if result["success"]:
926
1708
  data = result["data"]
927
1709
  reservations = data.get("results", []) if isinstance(data, dict) else data
928
-
1710
+
929
1711
  return {
930
1712
  "success": True,
931
1713
  "message": f"✅ Encontradas {len(reservations)} reservas de estoque",
932
1714
  "reservations": reservations,
933
- "count": len(reservations)
1715
+ "count": len(reservations),
934
1716
  }
935
-
1717
+
936
1718
  return {
937
1719
  "success": False,
938
1720
  "message": "❌ Erro ao buscar reservas de estoque",
939
1721
  "error": result.get("error"),
940
- "details": result.get("message")
1722
+ "details": result.get("message"),
941
1723
  }
942
1724
 
1725
+
943
1726
  # ============ PROJETOS/OBRAS ============
944
1727
 
1728
+
945
1729
  @mcp.tool
946
- async def get_sienge_projects(limit: Optional[int] = 100, offset: Optional[int] = 0, company_id: Optional[int] = None,
947
- enterprise_type: Optional[int] = None, receivable_register: Optional[str] = None,
948
- only_buildings_enabled: Optional[bool] = False) -> Dict:
1730
+ async def get_sienge_projects(
1731
+ limit: Optional[int] = 100,
1732
+ offset: Optional[int] = 0,
1733
+ company_id: Optional[int] = None,
1734
+ enterprise_type: Optional[int] = None,
1735
+ receivable_register: Optional[str] = None,
1736
+ only_buildings_enabled: Optional[bool] = False,
1737
+ ) -> Dict:
949
1738
  """
950
1739
  Busca empreendimentos/obras no Sienge
951
-
1740
+ CORRIGIDO: Mapeamento correto da chave de resposta
1741
+
952
1742
  Args:
953
1743
  limit: Máximo de registros (padrão: 100, máximo: 200)
954
1744
  offset: Pular registros (padrão: 0)
@@ -957,130 +1747,149 @@ async def get_sienge_projects(limit: Optional[int] = 100, offset: Optional[int]
957
1747
  receivable_register: Filtro de registro de recebíveis (B3, CERC)
958
1748
  only_buildings_enabled: Retornar apenas obras habilitadas para integração orçamentária
959
1749
  """
960
- params = {"limit": min(limit or 100, 200), "offset": offset or 0}
961
-
962
- if company_id:
963
- params["companyId"] = company_id
964
- if enterprise_type:
965
- params["type"] = enterprise_type
966
- if receivable_register:
967
- params["receivableRegister"] = receivable_register
968
- if only_buildings_enabled:
969
- params["onlyBuildingsEnabledForIntegration"] = only_buildings_enabled
970
-
971
- result = await make_sienge_request("GET", "/enterprises", params=params)
972
-
1750
+ # Usar serviço interno
1751
+ result = await _svc_get_projects(
1752
+ limit=limit or 100,
1753
+ offset=offset or 0,
1754
+ company_id=company_id,
1755
+ enterprise_type=enterprise_type,
1756
+ receivable_register=receivable_register,
1757
+ only_buildings_enabled=only_buildings_enabled or False
1758
+ )
1759
+
973
1760
  if result["success"]:
974
1761
  data = result["data"]
1762
+ # CORREÇÃO: API retorna em "results", mas paginador espera em "enterprises"
975
1763
  enterprises = data.get("results", []) if isinstance(data, dict) else data
976
1764
  metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
977
-
1765
+ total_count = metadata.get("count", len(enterprises))
1766
+
978
1767
  return {
979
1768
  "success": True,
980
- "message": f"✅ Encontrados {len(enterprises)} empreendimentos",
981
- "enterprises": enterprises,
1769
+ "message": f"✅ Encontrados {len(enterprises)} empreendimentos (total: {total_count})",
1770
+ "enterprises": enterprises, # Manter consistência para paginador
1771
+ "projects": enterprises, # Alias para compatibilidade
982
1772
  "count": len(enterprises),
1773
+ "total_count": total_count,
983
1774
  "metadata": metadata,
984
- "filters": params
1775
+ "filters_applied": {
1776
+ "limit": limit, "offset": offset, "company_id": company_id,
1777
+ "enterprise_type": enterprise_type, "receivable_register": receivable_register,
1778
+ "only_buildings_enabled": only_buildings_enabled
1779
+ },
1780
+ "request_id": result.get("request_id"),
1781
+ "latency_ms": result.get("latency_ms"),
1782
+ "cache": result.get("cache")
985
1783
  }
986
-
1784
+
987
1785
  return {
988
1786
  "success": False,
989
1787
  "message": "❌ Erro ao buscar empreendimentos",
990
1788
  "error": result.get("error"),
991
- "details": result.get("message")
1789
+ "details": result.get("message"),
1790
+ "request_id": result.get("request_id")
992
1791
  }
993
1792
 
1793
+
994
1794
  @mcp.tool
995
1795
  async def get_sienge_enterprise_by_id(enterprise_id: int) -> Dict:
996
1796
  """
997
1797
  Busca um empreendimento específico por ID no Sienge
998
-
1798
+
999
1799
  Args:
1000
1800
  enterprise_id: ID do empreendimento
1001
1801
  """
1002
1802
  result = await make_sienge_request("GET", f"/enterprises/{enterprise_id}")
1003
-
1803
+
1004
1804
  if result["success"]:
1005
- return {
1006
- "success": True,
1007
- "message": f"✅ Empreendimento {enterprise_id} encontrado",
1008
- "enterprise": result["data"]
1009
- }
1010
-
1805
+ return {"success": True, "message": f"✅ Empreendimento {enterprise_id} encontrado", "enterprise": result["data"]}
1806
+
1011
1807
  return {
1012
1808
  "success": False,
1013
1809
  "message": f"❌ Erro ao buscar empreendimento {enterprise_id}",
1014
1810
  "error": result.get("error"),
1015
- "details": result.get("message")
1811
+ "details": result.get("message"),
1016
1812
  }
1017
1813
 
1814
+
1018
1815
  @mcp.tool
1019
1816
  async def get_sienge_enterprise_groupings(enterprise_id: int) -> Dict:
1020
1817
  """
1021
1818
  Busca agrupamentos de unidades de um empreendimento específico
1022
-
1819
+
1023
1820
  Args:
1024
1821
  enterprise_id: ID do empreendimento
1025
1822
  """
1026
1823
  result = await make_sienge_request("GET", f"/enterprises/{enterprise_id}/groupings")
1027
-
1824
+
1028
1825
  if result["success"]:
1029
1826
  groupings = result["data"]
1030
1827
  return {
1031
1828
  "success": True,
1032
1829
  "message": f"✅ Agrupamentos do empreendimento {enterprise_id} encontrados",
1033
1830
  "groupings": groupings,
1034
- "count": len(groupings) if isinstance(groupings, list) else 0
1831
+ "count": len(groupings) if isinstance(groupings, list) else 0,
1035
1832
  }
1036
-
1833
+
1037
1834
  return {
1038
1835
  "success": False,
1039
1836
  "message": f"❌ Erro ao buscar agrupamentos do empreendimento {enterprise_id}",
1040
1837
  "error": result.get("error"),
1041
- "details": result.get("message")
1838
+ "details": result.get("message"),
1042
1839
  }
1043
1840
 
1841
+
1044
1842
  @mcp.tool
1045
1843
  async def get_sienge_units(limit: Optional[int] = 50, offset: Optional[int] = 0) -> Dict:
1046
1844
  """
1047
1845
  Consulta unidades cadastradas no Sienge
1048
-
1846
+
1049
1847
  Args:
1050
1848
  limit: Máximo de registros (padrão: 50)
1051
1849
  offset: Pular registros (padrão: 0)
1052
1850
  """
1053
1851
  params = {"limit": min(limit or 50, 200), "offset": offset or 0}
1054
1852
  result = await make_sienge_request("GET", "/units", params=params)
1055
-
1853
+
1056
1854
  if result["success"]:
1057
1855
  data = result["data"]
1058
1856
  units = data.get("results", []) if isinstance(data, dict) else data
1059
1857
  metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
1060
1858
  total_count = metadata.get("count", len(units))
1061
-
1859
+
1062
1860
  return {
1063
1861
  "success": True,
1064
1862
  "message": f"✅ Encontradas {len(units)} unidades (total: {total_count})",
1065
1863
  "units": units,
1066
- "count": len(units)
1864
+ "count": len(units),
1865
+ "total_count": total_count,
1866
+ "request_id": result.get("request_id"),
1867
+ "latency_ms": result.get("latency_ms")
1067
1868
  }
1068
-
1869
+
1069
1870
  return {
1070
1871
  "success": False,
1071
1872
  "message": "❌ Erro ao buscar unidades",
1072
1873
  "error": result.get("error"),
1073
- "details": result.get("message")
1874
+ "details": result.get("message"),
1875
+ "request_id": result.get("request_id"),
1876
+ "latency_ms": result.get("latency_ms")
1074
1877
  }
1075
1878
 
1879
+
1076
1880
  # ============ CUSTOS ============
1077
1881
 
1882
+
1078
1883
  @mcp.tool
1079
- async def get_sienge_unit_cost_tables(table_code: Optional[str] = None, description: Optional[str] = None,
1080
- status: Optional[str] = "Active", integration_enabled: Optional[bool] = None) -> Dict:
1884
+ async def get_sienge_unit_cost_tables(
1885
+ table_code: Optional[str] = None,
1886
+ description: Optional[str] = None,
1887
+ status: Optional[str] = "Active",
1888
+ integration_enabled: Optional[bool] = None,
1889
+ ) -> Dict:
1081
1890
  """
1082
1891
  Consulta tabelas de custos unitários
1083
-
1892
+
1084
1893
  Args:
1085
1894
  table_code: Código da tabela (opcional)
1086
1895
  description: Descrição da tabela (opcional)
@@ -1088,80 +1897,763 @@ async def get_sienge_unit_cost_tables(table_code: Optional[str] = None, descript
1088
1897
  integration_enabled: Se habilitada para integração
1089
1898
  """
1090
1899
  params = {"status": status or "Active"}
1091
-
1900
+
1092
1901
  if table_code:
1093
1902
  params["table_code"] = table_code
1094
1903
  if description:
1095
1904
  params["description"] = description
1096
1905
  if integration_enabled is not None:
1097
1906
  params["integration_enabled"] = integration_enabled
1098
-
1907
+
1099
1908
  result = await make_sienge_request("GET", "/unit-cost-tables", params=params)
1100
-
1909
+
1101
1910
  if result["success"]:
1102
1911
  data = result["data"]
1103
1912
  tables = data.get("results", []) if isinstance(data, dict) else data
1104
-
1913
+
1105
1914
  return {
1106
1915
  "success": True,
1107
1916
  "message": f"✅ Encontradas {len(tables)} tabelas de custos",
1108
1917
  "cost_tables": tables,
1109
- "count": len(tables)
1918
+ "count": len(tables),
1110
1919
  }
1111
-
1920
+
1112
1921
  return {
1113
1922
  "success": False,
1114
1923
  "message": "❌ Erro ao buscar tabelas de custos",
1115
1924
  "error": result.get("error"),
1116
- "details": result.get("message")
1925
+ "details": result.get("message"),
1926
+ }
1927
+
1928
+
1929
+ # ============ SEARCH UNIVERSAL (COMPATIBILIDADE CHATGPT) ============
1930
+
1931
+
1932
+ @mcp.tool
1933
+ async def search_sienge_data(
1934
+ query: str,
1935
+ entity_type: Optional[str] = None,
1936
+ limit: Optional[int] = 20,
1937
+ filters: Optional[Dict[str, Any]] = None
1938
+ ) -> Dict:
1939
+ """
1940
+ Busca universal no Sienge - compatível com ChatGPT/OpenAI MCP
1941
+
1942
+ Permite buscar em múltiplas entidades do Sienge de forma unificada.
1943
+
1944
+ Args:
1945
+ query: Termo de busca (nome, código, descrição, etc.)
1946
+ entity_type: Tipo de entidade (customers, creditors, projects, bills, purchase_orders, etc.)
1947
+ limit: Máximo de registros (padrão: 20, máximo: 100)
1948
+ filters: Filtros específicos por tipo de entidade
1949
+ """
1950
+ search_results = []
1951
+ limit = min(limit or 20, 100)
1952
+
1953
+ # Se entity_type específico, buscar apenas nele
1954
+ if entity_type:
1955
+ result = await _search_specific_entity(entity_type, query, limit, filters or {})
1956
+ if result["success"]:
1957
+ return result
1958
+ else:
1959
+ return {
1960
+ "success": False,
1961
+ "message": f"❌ Erro na busca em {entity_type}",
1962
+ "error": result.get("error"),
1963
+ "query": query,
1964
+ "entity_type": entity_type
1965
+ }
1966
+
1967
+ # Busca universal em múltiplas entidades
1968
+ entities_to_search = [
1969
+ ("customers", "clientes"),
1970
+ ("creditors", "credores/fornecedores"),
1971
+ ("projects", "empreendimentos/obras"),
1972
+ ("bills", "títulos a pagar"),
1973
+ ("purchase_orders", "pedidos de compra")
1974
+ ]
1975
+
1976
+ total_found = 0
1977
+
1978
+ for entity_key, entity_name in entities_to_search:
1979
+ try:
1980
+ entity_result = await _search_specific_entity(entity_key, query, min(5, limit), {})
1981
+ if entity_result["success"] and entity_result.get("count", 0) > 0:
1982
+ search_results.append({
1983
+ "entity_type": entity_key,
1984
+ "entity_name": entity_name,
1985
+ "count": entity_result["count"],
1986
+ "results": entity_result["data"][:5], # Limitar a 5 por entidade na busca universal
1987
+ "has_more": entity_result["count"] > 5
1988
+ })
1989
+ total_found += entity_result["count"]
1990
+ except Exception as e:
1991
+ # Continuar com outras entidades se uma falhar
1992
+ continue
1993
+
1994
+ if search_results:
1995
+ return {
1996
+ "success": True,
1997
+ "message": f"✅ Busca '{query}' encontrou resultados em {len(search_results)} entidades (total: {total_found} registros)",
1998
+ "query": query,
1999
+ "total_entities": len(search_results),
2000
+ "total_records": total_found,
2001
+ "results_by_entity": search_results,
2002
+ "suggestion": "Use entity_type para buscar especificamente em uma entidade e obter mais resultados"
2003
+ }
2004
+ else:
2005
+ return {
2006
+ "success": False,
2007
+ "message": f"❌ Nenhum resultado encontrado para '{query}'",
2008
+ "query": query,
2009
+ "searched_entities": [name for _, name in entities_to_search],
2010
+ "suggestion": "Tente termos mais específicos ou use os tools específicos de cada entidade"
2011
+ }
2012
+
2013
+
2014
+ async def _search_specific_entity(entity_type: str, query: str, limit: int, filters: Dict) -> Dict:
2015
+ """
2016
+ Função auxiliar para buscar em uma entidade específica
2017
+ CORRIGIDO: Usa serviços internos, nunca outras tools
2018
+ """
2019
+
2020
+ if entity_type == "customers":
2021
+ result = await _svc_get_customers(limit=limit, search=query)
2022
+ if result["success"]:
2023
+ data = result["data"]
2024
+ customers = data.get("results", []) if isinstance(data, dict) else data
2025
+ return {
2026
+ "success": True,
2027
+ "data": customers,
2028
+ "count": len(customers),
2029
+ "entity_type": "customers"
2030
+ }
2031
+
2032
+ elif entity_type == "creditors":
2033
+ result = await _svc_get_creditors(limit=limit, search=query)
2034
+ if result["success"]:
2035
+ data = result["data"]
2036
+ creditors = data.get("results", []) if isinstance(data, dict) else data
2037
+ return {
2038
+ "success": True,
2039
+ "data": creditors,
2040
+ "count": len(creditors),
2041
+ "entity_type": "creditors"
2042
+ }
2043
+
2044
+ elif entity_type == "projects" or entity_type == "enterprises":
2045
+ # Para projetos, usar filtros mais específicos se disponível
2046
+ company_id = filters.get("company_id")
2047
+ result = await _svc_get_projects(limit=limit, company_id=company_id)
2048
+ if result["success"]:
2049
+ data = result["data"]
2050
+ projects = data.get("results", []) if isinstance(data, dict) else data
2051
+
2052
+ # Filtrar por query se fornecida
2053
+ if query:
2054
+ projects = [
2055
+ p for p in projects
2056
+ if query.lower() in str(p.get("description", "")).lower()
2057
+ or query.lower() in str(p.get("name", "")).lower()
2058
+ or query.lower() in str(p.get("code", "")).lower()
2059
+ ]
2060
+ return {
2061
+ "success": True,
2062
+ "data": projects,
2063
+ "count": len(projects),
2064
+ "entity_type": "projects"
2065
+ }
2066
+
2067
+ elif entity_type == "bills":
2068
+ # Para títulos, usar data padrão se não especificada
2069
+ start_date = filters.get("start_date")
2070
+ end_date = filters.get("end_date")
2071
+ result = await _svc_get_bills(
2072
+ start_date=start_date,
2073
+ end_date=end_date,
2074
+ limit=limit
2075
+ )
2076
+ if result["success"]:
2077
+ data = result["data"]
2078
+ bills = data.get("results", []) if isinstance(data, dict) else data
2079
+ return {
2080
+ "success": True,
2081
+ "data": bills,
2082
+ "count": len(bills),
2083
+ "entity_type": "bills"
2084
+ }
2085
+
2086
+ elif entity_type == "purchase_orders":
2087
+ result = await _svc_get_purchase_orders(limit=limit)
2088
+ if result["success"]:
2089
+ data = result["data"]
2090
+ orders = data.get("results", []) if isinstance(data, dict) else data
2091
+
2092
+ # Filtrar por query se fornecida
2093
+ if query:
2094
+ orders = [
2095
+ o for o in orders
2096
+ if query.lower() in str(o.get("description", "")).lower()
2097
+ or query.lower() in str(o.get("id", "")).lower()
2098
+ ]
2099
+ return {
2100
+ "success": True,
2101
+ "data": orders,
2102
+ "count": len(orders),
2103
+ "entity_type": "purchase_orders"
2104
+ }
2105
+
2106
+ # Se chegou aqui, entidade não suportada ou erro
2107
+ return {
2108
+ "success": False,
2109
+ "error": f"Entidade '{entity_type}' não suportada ou erro na busca",
2110
+ "supported_entities": ["customers", "creditors", "projects", "bills", "purchase_orders"]
1117
2111
  }
1118
2112
 
2113
+
2114
+ @mcp.tool
2115
+ async def list_sienge_entities() -> Dict:
2116
+ """
2117
+ Lista todas as entidades disponíveis no Sienge MCP para busca
2118
+
2119
+ Retorna informações sobre os tipos de dados que podem ser consultados
2120
+ """
2121
+ entities = [
2122
+ {
2123
+ "type": "customers",
2124
+ "name": "Clientes",
2125
+ "description": "Clientes cadastrados no sistema",
2126
+ "search_fields": ["nome", "documento", "email"],
2127
+ "tools": ["get_sienge_customers", "search_sienge_data"]
2128
+ },
2129
+ {
2130
+ "type": "creditors",
2131
+ "name": "Credores/Fornecedores",
2132
+ "description": "Fornecedores e credores cadastrados",
2133
+ "search_fields": ["nome", "documento"],
2134
+ "tools": ["get_sienge_creditors", "get_sienge_creditor_bank_info", "get_sienge_suppliers"]
2135
+ },
2136
+ {
2137
+ "type": "projects",
2138
+ "name": "Empreendimentos/Obras",
2139
+ "description": "Projetos e obras cadastrados",
2140
+ "search_fields": ["código", "descrição", "nome"],
2141
+ "tools": ["get_sienge_projects", "get_sienge_enterprises", "get_sienge_enterprise_by_id"]
2142
+ },
2143
+ {
2144
+ "type": "bills",
2145
+ "name": "Títulos a Pagar",
2146
+ "description": "Contas a pagar e títulos financeiros",
2147
+ "search_fields": ["número", "credor", "valor"],
2148
+ "tools": ["get_sienge_bills", "get_sienge_accounts_payable"]
2149
+ },
2150
+ {
2151
+ "type": "purchase_orders",
2152
+ "name": "Pedidos de Compra",
2153
+ "description": "Pedidos de compra e solicitações",
2154
+ "search_fields": ["id", "descrição", "status"],
2155
+ "tools": ["get_sienge_purchase_orders", "get_sienge_purchase_requests", "list_sienge_purchase_requests"]
2156
+ },
2157
+ {
2158
+ "type": "invoices",
2159
+ "name": "Notas Fiscais",
2160
+ "description": "Notas fiscais de compra",
2161
+ "search_fields": ["número", "série", "fornecedor"],
2162
+ "tools": ["get_sienge_purchase_invoice"]
2163
+ },
2164
+ {
2165
+ "type": "stock",
2166
+ "name": "Estoque",
2167
+ "description": "Inventário e movimentações de estoque",
2168
+ "search_fields": ["centro_custo", "recurso"],
2169
+ "tools": ["get_sienge_stock_inventory", "get_sienge_stock_reservations"]
2170
+ },
2171
+ {
2172
+ "type": "financial",
2173
+ "name": "Financeiro",
2174
+ "description": "Contas a receber e movimentações financeiras",
2175
+ "search_fields": ["período", "cliente", "valor"],
2176
+ "tools": ["get_sienge_accounts_receivable", "search_sienge_financial_data", "search_sienge_finances"]
2177
+ },
2178
+ {
2179
+ "type": "suppliers",
2180
+ "name": "Fornecedores",
2181
+ "description": "Fornecedores e credores cadastrados",
2182
+ "search_fields": ["código", "nome", "razão social"],
2183
+ "tools": ["get_sienge_suppliers"]
2184
+ }
2185
+ ]
2186
+
2187
+ return {
2188
+ "success": True,
2189
+ "message": f"✅ {len(entities)} tipos de entidades disponíveis no Sienge",
2190
+ "entities": entities,
2191
+ "total_tools": sum(len(e["tools"]) for e in entities),
2192
+ "usage_example": {
2193
+ "search_all": "search_sienge_data('nome_cliente')",
2194
+ "search_specific": "search_sienge_data('nome_cliente', entity_type='customers')",
2195
+ "direct_access": "get_sienge_customers(search='nome_cliente')"
2196
+ }
2197
+ }
2198
+
2199
+
2200
+ # ============ PAGINATION E NAVEGAÇÃO ============
2201
+
2202
+
2203
+ @mcp.tool
2204
+ async def get_sienge_data_paginated(
2205
+ entity_type: str,
2206
+ page: int = 1,
2207
+ page_size: int = 20,
2208
+ filters: Optional[Dict[str, Any]] = None,
2209
+ sort_by: Optional[str] = None
2210
+ ) -> Dict:
2211
+ """
2212
+ Busca dados do Sienge com paginação avançada - compatível com ChatGPT
2213
+
2214
+ Args:
2215
+ entity_type: Tipo de entidade (customers, creditors, projects, bills, etc.)
2216
+ page: Número da página (começando em 1)
2217
+ page_size: Registros por página (máximo 50)
2218
+ filters: Filtros específicos da entidade
2219
+ sort_by: Campo para ordenação (se suportado)
2220
+ """
2221
+ page_size = min(page_size, 50)
2222
+ offset = (page - 1) * page_size
2223
+
2224
+ filters = filters or {}
2225
+
2226
+ # CORRIGIDO: Mapear para serviços internos, não tools
2227
+ if entity_type == "customers":
2228
+ search = filters.get("search")
2229
+ customer_type_id = filters.get("customer_type_id")
2230
+ result = await _svc_get_customers(
2231
+ limit=page_size,
2232
+ offset=offset,
2233
+ search=search,
2234
+ customer_type_id=customer_type_id
2235
+ )
2236
+ # CORRIGIDO: Extrair items e total corretamente
2237
+ if result["success"]:
2238
+ data = result["data"]
2239
+ items, total = _extract_items_and_total(data)
2240
+ result["customers"] = items
2241
+ result["count"] = len(items)
2242
+ result["total_count"] = total
2243
+
2244
+ elif entity_type == "creditors":
2245
+ search = filters.get("search")
2246
+ result = await _svc_get_creditors(
2247
+ limit=page_size,
2248
+ offset=offset,
2249
+ search=search
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["creditors"] = items
2256
+ result["count"] = len(items)
2257
+ result["total_count"] = total
2258
+
2259
+ elif entity_type == "projects":
2260
+ result = await _svc_get_projects(
2261
+ limit=page_size,
2262
+ offset=offset,
2263
+ company_id=filters.get("company_id"),
2264
+ enterprise_type=filters.get("enterprise_type")
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["projects"] = items
2271
+ result["enterprises"] = items # Para compatibilidade
2272
+ result["count"] = len(items)
2273
+ result["total_count"] = total
2274
+
2275
+ elif entity_type == "bills":
2276
+ result = await _svc_get_bills(
2277
+ start_date=filters.get("start_date"),
2278
+ end_date=filters.get("end_date"),
2279
+ creditor_id=filters.get("creditor_id"),
2280
+ status=filters.get("status"),
2281
+ limit=page_size
2282
+ )
2283
+ # CORRIGIDO: Extrair items e total corretamente
2284
+ if result["success"]:
2285
+ data = result["data"]
2286
+ items, total = _extract_items_and_total(data)
2287
+ result["bills"] = items
2288
+ result["count"] = len(items)
2289
+ result["total_count"] = total
2290
+
2291
+ else:
2292
+ return {
2293
+ "success": False,
2294
+ "message": f"❌ Tipo de entidade '{entity_type}' não suportado para paginação",
2295
+ "supported_types": ["customers", "creditors", "projects", "bills"]
2296
+ }
2297
+
2298
+ if result["success"]:
2299
+ # Calcular informações de paginação
2300
+ total_count = result.get("total_count", result.get("count", 0))
2301
+ total_pages = (total_count + page_size - 1) // page_size if total_count > 0 else 1
2302
+
2303
+ return {
2304
+ "success": True,
2305
+ "message": f"✅ Página {page} de {total_pages} - {entity_type}",
2306
+ "data": result.get(entity_type, result.get("data", [])),
2307
+ "pagination": {
2308
+ "current_page": page,
2309
+ "page_size": page_size,
2310
+ "total_pages": total_pages,
2311
+ "total_records": total_count,
2312
+ "has_next": page < total_pages,
2313
+ "has_previous": page > 1,
2314
+ "next_page": page + 1 if page < total_pages else None,
2315
+ "previous_page": page - 1 if page > 1 else None
2316
+ },
2317
+ "entity_type": entity_type,
2318
+ "filters_applied": filters
2319
+ }
2320
+
2321
+ return result
2322
+
2323
+
2324
+ @mcp.tool
2325
+ async def search_sienge_financial_data(
2326
+ period_start: str,
2327
+ period_end: str,
2328
+ search_type: str = "both",
2329
+ amount_min: Optional[float] = None,
2330
+ amount_max: Optional[float] = None,
2331
+ customer_creditor_search: Optional[str] = None
2332
+ ) -> Dict:
2333
+ """
2334
+ Busca avançada em dados financeiros do Sienge - Contas a Pagar e Receber
2335
+
2336
+ Args:
2337
+ period_start: Data inicial do período (YYYY-MM-DD)
2338
+ period_end: Data final do período (YYYY-MM-DD)
2339
+ search_type: Tipo de busca ("receivable", "payable", "both")
2340
+ amount_min: Valor mínimo (opcional)
2341
+ amount_max: Valor máximo (opcional)
2342
+ customer_creditor_search: Buscar por nome de cliente/credor (opcional)
2343
+ """
2344
+
2345
+ financial_results = {
2346
+ "receivable": {"success": False, "data": [], "count": 0, "error": None},
2347
+ "payable": {"success": False, "data": [], "count": 0, "error": None}
2348
+ }
2349
+
2350
+ # Buscar contas a receber
2351
+ if search_type in ["receivable", "both"]:
2352
+ try:
2353
+ # CORRIGIDO: Usar serviço interno
2354
+ receivable_result = await _svc_get_accounts_receivable(
2355
+ start_date=period_start,
2356
+ end_date=period_end,
2357
+ selection_type="D" # Por vencimento
2358
+ )
2359
+
2360
+ if receivable_result["success"]:
2361
+ receivable_data = receivable_result.get("data", [])
2362
+
2363
+ # CORRIGIDO: Aplicar filtros de valor usando _parse_numeric_value
2364
+ if amount_min is not None or amount_max is not None:
2365
+ filtered_data = []
2366
+ for item in receivable_data:
2367
+ amount = _parse_numeric_value(item.get("amount", 0))
2368
+ if amount_min is not None and amount < amount_min:
2369
+ continue
2370
+ if amount_max is not None and amount > amount_max:
2371
+ continue
2372
+ filtered_data.append(item)
2373
+ receivable_data = filtered_data
2374
+
2375
+ # Aplicar filtro de cliente se especificado
2376
+ if customer_creditor_search:
2377
+ search_lower = customer_creditor_search.lower()
2378
+ filtered_data = []
2379
+ for item in receivable_data:
2380
+ customer_name = str(item.get("customer_name", "")).lower()
2381
+ if search_lower in customer_name:
2382
+ filtered_data.append(item)
2383
+ receivable_data = filtered_data
2384
+
2385
+ financial_results["receivable"] = {
2386
+ "success": True,
2387
+ "data": receivable_data,
2388
+ "count": len(receivable_data),
2389
+ "error": None
2390
+ }
2391
+ else:
2392
+ financial_results["receivable"]["error"] = receivable_result.get("error")
2393
+
2394
+ except Exception as e:
2395
+ financial_results["receivable"]["error"] = str(e)
2396
+
2397
+ # Buscar contas a pagar
2398
+ if search_type in ["payable", "both"]:
2399
+ try:
2400
+ # CORRIGIDO: Usar serviço interno
2401
+ payable_result = await _svc_get_bills(
2402
+ start_date=period_start,
2403
+ end_date=period_end,
2404
+ limit=100
2405
+ )
2406
+
2407
+ if payable_result["success"]:
2408
+ data = payable_result["data"]
2409
+ payable_data = data.get("results", []) if isinstance(data, dict) else data
2410
+
2411
+ # CORRIGIDO: Aplicar filtros de valor usando _parse_numeric_value
2412
+ if amount_min is not None or amount_max is not None:
2413
+ filtered_data = []
2414
+ for item in payable_data:
2415
+ amount = _parse_numeric_value(item.get("amount", 0))
2416
+ if amount_min is not None and amount < amount_min:
2417
+ continue
2418
+ if amount_max is not None and amount > amount_max:
2419
+ continue
2420
+ filtered_data.append(item)
2421
+ payable_data = filtered_data
2422
+
2423
+ # Aplicar filtro de credor se especificado
2424
+ if customer_creditor_search:
2425
+ search_lower = customer_creditor_search.lower()
2426
+ filtered_data = []
2427
+ for item in payable_data:
2428
+ creditor_name = str(item.get("creditor_name", "")).lower()
2429
+ if search_lower in creditor_name:
2430
+ filtered_data.append(item)
2431
+ payable_data = filtered_data
2432
+
2433
+ financial_results["payable"] = {
2434
+ "success": True,
2435
+ "data": payable_data,
2436
+ "count": len(payable_data),
2437
+ "error": None
2438
+ }
2439
+ else:
2440
+ financial_results["payable"]["error"] = payable_result.get("error")
2441
+
2442
+ except Exception as e:
2443
+ financial_results["payable"]["error"] = str(e)
2444
+
2445
+ # Compilar resultado final
2446
+ total_records = financial_results["receivable"]["count"] + financial_results["payable"]["count"]
2447
+ has_errors = bool(financial_results["receivable"]["error"] or financial_results["payable"]["error"])
2448
+
2449
+ summary = {
2450
+ "period": f"{period_start} a {period_end}",
2451
+ "search_type": search_type,
2452
+ "total_records": total_records,
2453
+ "receivable_count": financial_results["receivable"]["count"],
2454
+ "payable_count": financial_results["payable"]["count"],
2455
+ "filters_applied": {
2456
+ "amount_range": f"{amount_min or 'sem mín'} - {amount_max or 'sem máx'}",
2457
+ "customer_creditor": customer_creditor_search or "todos"
2458
+ }
2459
+ }
2460
+
2461
+ if total_records > 0:
2462
+ return {
2463
+ "success": True,
2464
+ "message": f"✅ Busca financeira encontrou {total_records} registros no período",
2465
+ "summary": summary,
2466
+ "receivable": financial_results["receivable"],
2467
+ "payable": financial_results["payable"],
2468
+ "has_errors": has_errors
2469
+ }
2470
+ else:
2471
+ return {
2472
+ "success": False,
2473
+ "message": f"❌ Nenhum registro financeiro encontrado no período {period_start} a {period_end}",
2474
+ "summary": summary,
2475
+ "errors": {
2476
+ "receivable": financial_results["receivable"]["error"],
2477
+ "payable": financial_results["payable"]["error"]
2478
+ }
2479
+ }
2480
+
2481
+
2482
+ @mcp.tool
2483
+ async def get_sienge_dashboard_summary() -> Dict:
2484
+ """
2485
+ Obtém um resumo tipo dashboard com informações gerais do Sienge
2486
+ Útil para visão geral rápida do sistema
2487
+ """
2488
+
2489
+ # Data atual e períodos
2490
+ today = datetime.now()
2491
+ current_month_start = today.replace(day=1).strftime("%Y-%m-%d")
2492
+ current_month_end = today.strftime("%Y-%m-%d")
2493
+
2494
+ dashboard_data = {}
2495
+ errors = []
2496
+
2497
+ # 1. Testar conexão
2498
+ try:
2499
+ connection_test = await test_sienge_connection()
2500
+ dashboard_data["connection"] = connection_test
2501
+ except Exception as e:
2502
+ errors.append(f"Teste de conexão: {str(e)}")
2503
+ dashboard_data["connection"] = {"success": False, "error": str(e)}
2504
+
2505
+ # 2. Contar clientes (amostra)
2506
+ try:
2507
+ # CORRIGIDO: Usar serviço interno
2508
+ customers_result = await _svc_get_customers(limit=1)
2509
+ dashboard_data["customers"] = {"available": customers_result["success"]}
2510
+ except Exception as e:
2511
+ errors.append(f"Clientes: {str(e)}")
2512
+ dashboard_data["customers"] = {"available": False}
2513
+
2514
+ # 3. Contar projetos (amostra)
2515
+ try:
2516
+ # CORRIGIDO: Usar serviço interno
2517
+ projects_result = await _svc_get_projects(limit=5)
2518
+ if projects_result["success"]:
2519
+ data = projects_result["data"]
2520
+ enterprises = data.get("results", []) if isinstance(data, dict) else data
2521
+ metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
2522
+
2523
+ dashboard_data["projects"] = {
2524
+ "available": True,
2525
+ "sample_count": len(enterprises),
2526
+ "total_count": metadata.get("count", "N/A")
2527
+ }
2528
+ else:
2529
+ dashboard_data["projects"] = {"available": False}
2530
+ except Exception as e:
2531
+ errors.append(f"Projetos: {str(e)}")
2532
+ dashboard_data["projects"] = {"available": False, "error": str(e)}
2533
+
2534
+ # 4. Títulos a pagar do mês atual
2535
+ try:
2536
+ # CORRIGIDO: Usar serviço interno
2537
+ bills_result = await _svc_get_bills(
2538
+ start_date=current_month_start,
2539
+ end_date=current_month_end,
2540
+ limit=10
2541
+ )
2542
+ if bills_result["success"]:
2543
+ data = bills_result["data"]
2544
+ bills = data.get("results", []) if isinstance(data, dict) else data
2545
+ metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
2546
+
2547
+ dashboard_data["monthly_bills"] = {
2548
+ "available": True,
2549
+ "count": len(bills),
2550
+ "total_count": metadata.get("count", len(bills))
2551
+ }
2552
+ else:
2553
+ dashboard_data["monthly_bills"] = {"available": False}
2554
+ except Exception as e:
2555
+ errors.append(f"Títulos mensais: {str(e)}")
2556
+ dashboard_data["monthly_bills"] = {"available": False, "error": str(e)}
2557
+
2558
+ # 5. Tipos de clientes
2559
+ try:
2560
+ # CORRIGIDO: Usar serviço interno
2561
+ customer_types_result = await _svc_get_customer_types()
2562
+ if customer_types_result["success"]:
2563
+ data = customer_types_result["data"]
2564
+ customer_types = data.get("results", []) if isinstance(data, dict) else data
2565
+
2566
+ dashboard_data["customer_types"] = {
2567
+ "available": True,
2568
+ "count": len(customer_types)
2569
+ }
2570
+ else:
2571
+ dashboard_data["customer_types"] = {"available": False}
2572
+ except Exception as e:
2573
+ dashboard_data["customer_types"] = {"available": False, "error": str(e)}
2574
+
2575
+ # Compilar resultado
2576
+ available_modules = sum(1 for key, value in dashboard_data.items()
2577
+ if key != "connection" and isinstance(value, dict) and value.get("available"))
2578
+
2579
+ return {
2580
+ "success": True,
2581
+ "message": f"✅ Dashboard do Sienge - {available_modules} módulos disponíveis",
2582
+ "timestamp": today.isoformat(),
2583
+ "period_analyzed": f"{current_month_start} a {current_month_end}",
2584
+ "modules_status": dashboard_data,
2585
+ "available_modules": available_modules,
2586
+ "errors": errors if errors else None,
2587
+ "quick_actions": [
2588
+ "search_sienge_data('termo_busca') - Busca universal",
2589
+ "list_sienge_entities() - Listar tipos de dados",
2590
+ "get_sienge_customers(search='nome') - Buscar clientes",
2591
+ "get_sienge_projects() - Listar projetos/obras",
2592
+ "search_sienge_financial_data('2024-01-01', '2024-12-31') - Dados financeiros"
2593
+ ]
2594
+ }
2595
+
2596
+
1119
2597
  # ============ UTILITÁRIOS ============
1120
2598
 
2599
+
1121
2600
  @mcp.tool
1122
2601
  def add(a: int, b: int) -> int:
1123
2602
  """Soma dois números (função de teste)"""
1124
2603
  return a + b
1125
2604
 
2605
+
2606
+ def _mask(s: str) -> str:
2607
+ """Mascara dados sensíveis mantendo apenas o início e fim"""
2608
+ if not s:
2609
+ return None
2610
+ if len(s) == 1:
2611
+ return s + "*"
2612
+ if len(s) == 2:
2613
+ return s
2614
+ if len(s) <= 4:
2615
+ return s[:2] + "*" * (len(s) - 2)
2616
+ # Para strings > 4: usar no máximo 4 asteriscos no meio
2617
+ middle_asterisks = min(4, len(s) - 4)
2618
+ return s[:2] + "*" * middle_asterisks + s[-2:]
2619
+
2620
+
1126
2621
  def _get_auth_info_internal() -> Dict:
1127
2622
  """Função interna para verificar configuração de autenticação"""
1128
2623
  if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
1129
- return {
1130
- "auth_method": "Bearer Token",
1131
- "configured": True,
1132
- "base_url": SIENGE_BASE_URL,
1133
- "api_key_configured": True
1134
- }
2624
+ return {"auth_method": "Bearer Token", "configured": True, "base_url": SIENGE_BASE_URL, "api_key_configured": True}
1135
2625
  elif SIENGE_USERNAME and SIENGE_PASSWORD:
1136
2626
  return {
1137
2627
  "auth_method": "Basic Auth",
1138
2628
  "configured": True,
1139
2629
  "base_url": SIENGE_BASE_URL,
1140
2630
  "subdomain": SIENGE_SUBDOMAIN,
1141
- "username": SIENGE_USERNAME
2631
+ "username": _mask(SIENGE_USERNAME) if SIENGE_USERNAME else None,
1142
2632
  }
1143
2633
  else:
1144
2634
  return {
1145
2635
  "auth_method": "None",
1146
2636
  "configured": False,
1147
- "message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env"
2637
+ "message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env",
1148
2638
  }
1149
2639
 
2640
+
1150
2641
  @mcp.tool
1151
2642
  def get_auth_info() -> Dict:
1152
2643
  """Retorna informações sobre a configuração de autenticação"""
1153
2644
  return _get_auth_info_internal()
1154
2645
 
2646
+
1155
2647
  def main():
1156
2648
  """Entry point for the Sienge MCP Server"""
1157
2649
  print("* Iniciando Sienge MCP Server (FastMCP)...")
1158
-
2650
+
1159
2651
  # Mostrar info de configuração
1160
2652
  auth_info = _get_auth_info_internal()
1161
2653
  print(f"* Autenticacao: {auth_info['auth_method']}")
1162
2654
  print(f"* Configurado: {auth_info['configured']}")
1163
-
1164
- if not auth_info['configured']:
2655
+
2656
+ if not auth_info["configured"]:
1165
2657
  print("* ERRO: Autenticacao nao configurada!")
1166
2658
  print("Configure as variáveis de ambiente:")
1167
2659
  print("- SIENGE_API_KEY (Bearer Token) OU")
@@ -1179,8 +2671,9 @@ def main():
1179
2671
  print('export SIENGE_SUBDOMAIN="sua_empresa"')
1180
2672
  else:
1181
2673
  print("* MCP pronto para uso!")
1182
-
2674
+
1183
2675
  mcp.run()
1184
2676
 
2677
+
1185
2678
  if __name__ == "__main__":
1186
- main()
2679
+ main()