sienge-ecbiesek-mcp 1.2.1__py3-none-any.whl → 1.2.3__py3-none-any.whl

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

Potentially problematic release.


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

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