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