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_ecbiesek_mcp-1.2.1.dist-info → sienge_ecbiesek_mcp-1.2.3.dist-info}/METADATA +1 -1
- sienge_ecbiesek_mcp-1.2.3.dist-info/RECORD +11 -0
- sienge_mcp/__init__.py +4 -4
- sienge_mcp/server.py +367 -1143
- sienge_mcp/server2.py +55 -13
- sienge_ecbiesek_mcp-1.2.1.dist-info/RECORD +0 -11
- {sienge_ecbiesek_mcp-1.2.1.dist-info → sienge_ecbiesek_mcp-1.2.3.dist-info}/WHEEL +0 -0
- {sienge_ecbiesek_mcp-1.2.1.dist-info → sienge_ecbiesek_mcp-1.2.3.dist-info}/entry_points.txt +0 -0
- {sienge_ecbiesek_mcp-1.2.1.dist-info → sienge_ecbiesek_mcp-1.2.3.dist-info}/licenses/LICENSE +0 -0
- {sienge_ecbiesek_mcp-1.2.1.dist-info → sienge_ecbiesek_mcp-1.2.3.dist-info}/top_level.txt +0 -0
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
|
|
9
|
+
from typing import Dict, List, Optional, Any
|
|
17
10
|
import os
|
|
18
11
|
from dotenv import load_dotenv
|
|
19
|
-
from datetime import datetime
|
|
20
|
-
import uuid
|
|
21
|
-
import asyncio
|
|
22
|
-
import json
|
|
12
|
+
from datetime import datetime
|
|
23
13
|
import time
|
|
24
|
-
import
|
|
25
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
172
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
"
|
|
284
|
-
"
|
|
111
|
+
"latency_ms": latency_ms,
|
|
112
|
+
"request_id": request_id,
|
|
285
113
|
}
|
|
286
114
|
|
|
287
115
|
except httpx.TimeoutException:
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
299
|
-
|
|
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
|
-
|
|
318
|
-
|
|
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
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
|
174
|
+
if response.status_code in [200, 201]:
|
|
362
175
|
try:
|
|
363
|
-
return {
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
"
|
|
385
|
-
"
|
|
185
|
+
"latency_ms": latency_ms,
|
|
186
|
+
"request_id": request_id,
|
|
386
187
|
}
|
|
387
188
|
|
|
388
189
|
except httpx.TimeoutException:
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
400
|
-
|
|
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
|
-
#
|
|
667
|
-
result = await
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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(
|
|
299
|
+
async def get_sienge_customer_types() -> Dict:
|
|
755
300
|
"""Lista tipos de clientes disponíveis"""
|
|
756
|
-
|
|
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
|
-
|
|
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
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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
|
|
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
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
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
|
-
|
|
1106
|
-
income_data =
|
|
1107
|
-
|
|
1108
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
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: {
|
|
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":
|
|
1259
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
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
|
-
"
|
|
833
|
+
"documentId": document_id,
|
|
1532
834
|
"number": number,
|
|
1533
|
-
"
|
|
1534
|
-
"
|
|
1535
|
-
"
|
|
1536
|
-
"
|
|
1537
|
-
"
|
|
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=
|
|
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
|
-
|
|
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
|
-
"
|
|
1583
|
-
"
|
|
1584
|
-
"
|
|
1585
|
-
"
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
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
|
|
1782
|
-
"enterprises": enterprises,
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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":
|
|
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
|
|
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":
|
|
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
|
|
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
|
|
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":
|
|
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
|
|
1373
|
+
result = await get_sienge_purchase_orders(limit=limit)
|
|
2103
1374
|
if result["success"]:
|
|
2104
|
-
|
|
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(
|
|
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"
|
|
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", "
|
|
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"
|
|
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"
|
|
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"
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1608
|
+
receivable_data = receivable_result["income_data"]
|
|
2377
1609
|
|
|
2378
|
-
#
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
2424
|
-
payable_data = data.get("results", []) if isinstance(data, dict) else data
|
|
1654
|
+
payable_data = payable_result["bills"]
|
|
2425
1655
|
|
|
2426
|
-
#
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
2523
|
-
customers_result
|
|
2524
|
-
|
|
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["
|
|
1759
|
+
dashboard_data["customers_available"] = False
|
|
2528
1760
|
|
|
2529
1761
|
# 3. Contar projetos (amostra)
|
|
2530
1762
|
try:
|
|
2531
|
-
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
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":
|
|
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()
|