sienge-ecbiesek-mcp 1.1.0__py3-none-any.whl → 1.1.5__py3-none-any.whl

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

Potentially problematic release.


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

sienge_mcp/server.py CHANGED
@@ -6,18 +6,15 @@ Suporta Bearer Token e Basic Auth
6
6
 
7
7
  from fastmcp import FastMCP
8
8
  import httpx
9
- import asyncio
10
9
  from typing import Dict, List, Optional, Any
11
- import json
12
10
  import os
13
11
  from dotenv import load_dotenv
14
12
  from datetime import datetime
15
- import base64
16
13
 
17
14
  # Carrega as variáveis de ambiente
18
15
  load_dotenv()
19
16
 
20
- mcp = FastMCP("Sienge API Integration 🏗️")
17
+ mcp = FastMCP("Sienge API Integration 🏗️ - ChatGPT Compatible")
21
18
 
22
19
  # Configurações da API do Sienge
23
20
  SIENGE_BASE_URL = os.getenv("SIENGE_BASE_URL", "https://api.sienge.com.br")
@@ -27,25 +24,27 @@ SIENGE_PASSWORD = os.getenv("SIENGE_PASSWORD", "")
27
24
  SIENGE_API_KEY = os.getenv("SIENGE_API_KEY", "")
28
25
  REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", "30"))
29
26
 
27
+
30
28
  class SiengeAPIError(Exception):
31
29
  """Exceção customizada para erros da API do Sienge"""
30
+
32
31
  pass
33
32
 
34
- async def make_sienge_request(method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None) -> Dict:
33
+
34
+ async def make_sienge_request(
35
+ method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None
36
+ ) -> Dict:
35
37
  """
36
38
  Função auxiliar para fazer requisições à API do Sienge (v1)
37
39
  Suporta tanto Bearer Token quanto Basic Auth
38
40
  """
39
41
  try:
40
42
  async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
41
- headers = {
42
- "Content-Type": "application/json",
43
- "Accept": "application/json"
44
- }
45
-
43
+ headers = {"Content-Type": "application/json", "Accept": "application/json"}
44
+
46
45
  # Configurar autenticação e URL
47
46
  auth = None
48
-
47
+
49
48
  if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
50
49
  # Bearer Token (Recomendado)
51
50
  headers["Authorization"] = f"Bearer {SIENGE_API_KEY}"
@@ -58,67 +57,44 @@ async def make_sienge_request(method: str, endpoint: str, params: Optional[Dict]
58
57
  return {
59
58
  "success": False,
60
59
  "error": "No Authentication",
61
- "message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env"
60
+ "message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env",
62
61
  }
63
-
64
- response = await client.request(
65
- method=method,
66
- url=url,
67
- headers=headers,
68
- params=params,
69
- json=json_data,
70
- auth=auth
71
- )
72
-
62
+
63
+ response = await client.request(method=method, url=url, headers=headers, params=params, json=json_data, auth=auth)
64
+
73
65
  if response.status_code in [200, 201]:
74
66
  try:
75
- return {
76
- "success": True,
77
- "data": response.json(),
78
- "status_code": response.status_code
79
- }
80
- except:
81
- return {
82
- "success": True,
83
- "data": {"message": "Success"},
84
- "status_code": response.status_code
85
- }
67
+ return {"success": True, "data": response.json(), "status_code": response.status_code}
68
+ except BaseException:
69
+ return {"success": True, "data": {"message": "Success"}, "status_code": response.status_code}
86
70
  else:
87
71
  return {
88
72
  "success": False,
89
73
  "error": f"HTTP {response.status_code}",
90
74
  "message": response.text,
91
- "status_code": response.status_code
75
+ "status_code": response.status_code,
92
76
  }
93
-
77
+
94
78
  except httpx.TimeoutException:
95
- return {
96
- "success": False,
97
- "error": "Timeout",
98
- "message": f"A requisição excedeu o tempo limite de {REQUEST_TIMEOUT}s"
99
- }
79
+ return {"success": False, "error": "Timeout", "message": f"A requisição excedeu o tempo limite de {REQUEST_TIMEOUT}s"}
100
80
  except Exception as e:
101
- return {
102
- "success": False,
103
- "error": str(e),
104
- "message": f"Erro na requisição: {str(e)}"
105
- }
81
+ return {"success": False, "error": str(e), "message": f"Erro na requisição: {str(e)}"}
106
82
 
107
- async def make_sienge_bulk_request(method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None) -> Dict:
83
+
84
+ async def make_sienge_bulk_request(
85
+ method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None
86
+ ) -> Dict:
108
87
  """
109
88
  Função auxiliar para fazer requisições à API bulk-data do Sienge
110
89
  Suporta tanto Bearer Token quanto Basic Auth
111
90
  """
112
91
  try:
113
92
  async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
114
- headers = {
115
- "Content-Type": "application/json",
116
- "Accept": "application/json"
117
- }
118
-
93
+ headers = {"Content-Type": "application/json", "Accept": "application/json"}
94
+
119
95
  # Configurar autenticação e URL para bulk-data
120
96
  auth = None
121
-
97
+
122
98
  if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
123
99
  # Bearer Token (Recomendado)
124
100
  headers["Authorization"] = f"Bearer {SIENGE_API_KEY}"
@@ -131,61 +107,40 @@ async def make_sienge_bulk_request(method: str, endpoint: str, params: Optional[
131
107
  return {
132
108
  "success": False,
133
109
  "error": "No Authentication",
134
- "message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env"
110
+ "message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env",
135
111
  }
136
-
137
- response = await client.request(
138
- method=method,
139
- url=url,
140
- headers=headers,
141
- params=params,
142
- json=json_data,
143
- auth=auth
144
- )
145
-
112
+
113
+ response = await client.request(method=method, url=url, headers=headers, params=params, json=json_data, auth=auth)
114
+
146
115
  if response.status_code in [200, 201]:
147
116
  try:
148
- return {
149
- "success": True,
150
- "data": response.json(),
151
- "status_code": response.status_code
152
- }
153
- except:
154
- return {
155
- "success": True,
156
- "data": {"message": "Success"},
157
- "status_code": response.status_code
158
- }
117
+ return {"success": True, "data": response.json(), "status_code": response.status_code}
118
+ except BaseException:
119
+ return {"success": True, "data": {"message": "Success"}, "status_code": response.status_code}
159
120
  else:
160
121
  return {
161
122
  "success": False,
162
123
  "error": f"HTTP {response.status_code}",
163
124
  "message": response.text,
164
- "status_code": response.status_code
125
+ "status_code": response.status_code,
165
126
  }
166
-
127
+
167
128
  except httpx.TimeoutException:
168
- return {
169
- "success": False,
170
- "error": "Timeout",
171
- "message": f"A requisição excedeu o tempo limite de {REQUEST_TIMEOUT}s"
172
- }
129
+ return {"success": False, "error": "Timeout", "message": f"A requisição excedeu o tempo limite de {REQUEST_TIMEOUT}s"}
173
130
  except Exception as e:
174
- return {
175
- "success": False,
176
- "error": str(e),
177
- "message": f"Erro na requisição bulk-data: {str(e)}"
178
- }
131
+ return {"success": False, "error": str(e), "message": f"Erro na requisição bulk-data: {str(e)}"}
132
+
179
133
 
180
134
  # ============ CONEXÃO E TESTE ============
181
135
 
136
+
182
137
  @mcp.tool
183
138
  async def test_sienge_connection() -> Dict:
184
139
  """Testa a conexão com a API do Sienge"""
185
140
  try:
186
141
  # Tentar endpoint mais simples primeiro
187
142
  result = await make_sienge_request("GET", "/customer-types")
188
-
143
+
189
144
  if result["success"]:
190
145
  auth_method = "Bearer Token" if SIENGE_API_KEY else "Basic Auth"
191
146
  return {
@@ -193,7 +148,7 @@ async def test_sienge_connection() -> Dict:
193
148
  "message": "✅ Conexão com API do Sienge estabelecida com sucesso!",
194
149
  "api_status": "Online",
195
150
  "auth_method": auth_method,
196
- "timestamp": datetime.now().isoformat()
151
+ "timestamp": datetime.now().isoformat(),
197
152
  }
198
153
  else:
199
154
  return {
@@ -201,23 +156,27 @@ async def test_sienge_connection() -> Dict:
201
156
  "message": "❌ Falha ao conectar com API do Sienge",
202
157
  "error": result.get("error"),
203
158
  "details": result.get("message"),
204
- "timestamp": datetime.now().isoformat()
159
+ "timestamp": datetime.now().isoformat(),
205
160
  }
206
161
  except Exception as e:
207
162
  return {
208
163
  "success": False,
209
164
  "message": "❌ Erro ao testar conexão",
210
165
  "error": str(e),
211
- "timestamp": datetime.now().isoformat()
166
+ "timestamp": datetime.now().isoformat(),
212
167
  }
213
168
 
169
+
214
170
  # ============ CLIENTES ============
215
171
 
172
+
216
173
  @mcp.tool
217
- async def get_sienge_customers(limit: Optional[int] = 50, offset: Optional[int] = 0, search: Optional[str] = None, customer_type_id: Optional[str] = None) -> Dict:
174
+ async def get_sienge_customers(
175
+ limit: Optional[int] = 50, offset: Optional[int] = 0, search: Optional[str] = None, customer_type_id: Optional[str] = None
176
+ ) -> Dict:
218
177
  """
219
178
  Busca clientes no Sienge com filtros
220
-
179
+
221
180
  Args:
222
181
  limit: Máximo de registros (padrão: 50)
223
182
  offset: Pular registros (padrão: 0)
@@ -225,67 +184,70 @@ async def get_sienge_customers(limit: Optional[int] = 50, offset: Optional[int]
225
184
  customer_type_id: Filtrar por tipo de cliente
226
185
  """
227
186
  params = {"limit": min(limit or 50, 200), "offset": offset or 0}
228
-
187
+
229
188
  if search:
230
189
  params["search"] = search
231
190
  if customer_type_id:
232
191
  params["customer_type_id"] = customer_type_id
233
-
192
+
234
193
  result = await make_sienge_request("GET", "/customers", params=params)
235
-
194
+
236
195
  if result["success"]:
237
196
  data = result["data"]
238
197
  customers = data.get("results", []) if isinstance(data, dict) else data
239
198
  metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
240
199
  total_count = metadata.get("count", len(customers))
241
-
200
+
242
201
  return {
243
202
  "success": True,
244
203
  "message": f"✅ Encontrados {len(customers)} clientes (total: {total_count})",
245
204
  "customers": customers,
246
205
  "count": len(customers),
247
- "filters_applied": params
206
+ "filters_applied": params,
248
207
  }
249
-
208
+
250
209
  return {
251
210
  "success": False,
252
211
  "message": "❌ Erro ao buscar clientes",
253
212
  "error": result.get("error"),
254
- "details": result.get("message")
213
+ "details": result.get("message"),
255
214
  }
256
215
 
216
+
257
217
  @mcp.tool
258
218
  async def get_sienge_customer_types() -> Dict:
259
219
  """Lista tipos de clientes disponíveis"""
260
220
  result = await make_sienge_request("GET", "/customer-types")
261
-
221
+
262
222
  if result["success"]:
263
223
  data = result["data"]
264
224
  customer_types = data.get("results", []) if isinstance(data, dict) else data
265
225
  metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
266
226
  total_count = metadata.get("count", len(customer_types))
267
-
227
+
268
228
  return {
269
229
  "success": True,
270
230
  "message": f"✅ Encontrados {len(customer_types)} tipos de clientes (total: {total_count})",
271
231
  "customer_types": customer_types,
272
- "count": len(customer_types)
232
+ "count": len(customer_types),
273
233
  }
274
-
234
+
275
235
  return {
276
236
  "success": False,
277
237
  "message": "❌ Erro ao buscar tipos de clientes",
278
238
  "error": result.get("error"),
279
- "details": result.get("message")
239
+ "details": result.get("message"),
280
240
  }
281
241
 
242
+
282
243
  # ============ CREDORES ============
283
244
 
245
+
284
246
  @mcp.tool
285
247
  async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int] = 0, search: Optional[str] = None) -> Dict:
286
248
  """
287
249
  Busca credores/fornecedores
288
-
250
+
289
251
  Args:
290
252
  limit: Máximo de registros (padrão: 50)
291
253
  offset: Pular registros (padrão: 0)
@@ -294,69 +256,80 @@ async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int]
294
256
  params = {"limit": min(limit or 50, 200), "offset": offset or 0}
295
257
  if search:
296
258
  params["search"] = search
297
-
259
+
298
260
  result = await make_sienge_request("GET", "/creditors", params=params)
299
-
261
+
300
262
  if result["success"]:
301
263
  data = result["data"]
302
264
  creditors = data.get("results", []) if isinstance(data, dict) else data
303
265
  metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
304
266
  total_count = metadata.get("count", len(creditors))
305
-
267
+
306
268
  return {
307
269
  "success": True,
308
270
  "message": f"✅ Encontrados {len(creditors)} credores (total: {total_count})",
309
271
  "creditors": creditors,
310
- "count": len(creditors)
272
+ "count": len(creditors),
311
273
  }
312
-
274
+
313
275
  return {
314
276
  "success": False,
315
277
  "message": "❌ Erro ao buscar credores",
316
278
  "error": result.get("error"),
317
- "details": result.get("message")
279
+ "details": result.get("message"),
318
280
  }
319
281
 
282
+
320
283
  @mcp.tool
321
284
  async def get_sienge_creditor_bank_info(creditor_id: str) -> Dict:
322
285
  """
323
286
  Consulta informações bancárias de um credor
324
-
287
+
325
288
  Args:
326
289
  creditor_id: ID do credor (obrigatório)
327
290
  """
328
291
  result = await make_sienge_request("GET", f"/creditors/{creditor_id}/bank-informations")
329
-
292
+
330
293
  if result["success"]:
331
294
  return {
332
295
  "success": True,
333
296
  "message": f"✅ Informações bancárias do credor {creditor_id}",
334
297
  "creditor_id": creditor_id,
335
- "bank_info": result["data"]
298
+ "bank_info": result["data"],
336
299
  }
337
-
300
+
338
301
  return {
339
302
  "success": False,
340
303
  "message": f"❌ Erro ao buscar info bancária do credor {creditor_id}",
341
304
  "error": result.get("error"),
342
- "details": result.get("message")
305
+ "details": result.get("message"),
343
306
  }
344
307
 
308
+
345
309
  # ============ FINANCEIRO ============
346
310
 
311
+
347
312
  @mcp.tool
348
- async def get_sienge_accounts_receivable(start_date: str, end_date: str, selection_type: str = "D",
349
- company_id: Optional[int] = None, cost_centers_id: Optional[List[int]] = None,
350
- correction_indexer_id: Optional[int] = None, correction_date: Optional[str] = None,
351
- change_start_date: Optional[str] = None, completed_bills: Optional[str] = None,
352
- origins_ids: Optional[List[str]] = None, bearers_id_in: Optional[List[int]] = None,
353
- bearers_id_not_in: Optional[List[int]] = None) -> Dict:
313
+ async def get_sienge_accounts_receivable(
314
+ start_date: str,
315
+ end_date: str,
316
+ selection_type: str = "D",
317
+ company_id: Optional[int] = None,
318
+ cost_centers_id: Optional[List[int]] = None,
319
+ correction_indexer_id: Optional[int] = None,
320
+ correction_date: Optional[str] = None,
321
+ change_start_date: Optional[str] = None,
322
+ completed_bills: Optional[str] = None,
323
+ origins_ids: Optional[List[str]] = None,
324
+ bearers_id_in: Optional[List[int]] = None,
325
+ bearers_id_not_in: Optional[List[int]] = None,
326
+ ) -> Dict:
354
327
  """
355
328
  Consulta parcelas do contas a receber via API bulk-data
356
-
329
+
357
330
  Args:
358
331
  start_date: Data de início do período (YYYY-MM-DD) - OBRIGATÓRIO
359
- end_date: Data do fim do período (YYYY-MM-DD) - OBRIGATÓRIO
332
+ end_date: Data do fim do período (YYYY-MM-DD) - OBRIGATÓRIO
360
333
  selection_type: Seleção da data do período (I=emissão, D=vencimento, P=pagamento, B=competência) - padrão: D
361
334
  company_id: Código da empresa
362
335
  cost_centers_id: Lista de códigos de centro de custo
@@ -368,12 +341,8 @@ async def get_sienge_accounts_receivable(start_date: str, end_date: str, selecti
368
341
  bearers_id_in: Filtrar parcelas com códigos de portador específicos
369
342
  bearers_id_not_in: Filtrar parcelas excluindo códigos de portador específicos
370
343
  """
371
- params = {
372
- "startDate": start_date,
373
- "endDate": end_date,
374
- "selectionType": selection_type
375
- }
376
-
344
+ params = {"startDate": start_date, "endDate": end_date, "selectionType": selection_type}
345
+
377
346
  if company_id:
378
347
  params["companyId"] = company_id
379
348
  if cost_centers_id:
@@ -392,13 +361,13 @@ async def get_sienge_accounts_receivable(start_date: str, end_date: str, selecti
392
361
  params["bearersIdIn"] = bearers_id_in
393
362
  if bearers_id_not_in:
394
363
  params["bearersIdNotIn"] = bearers_id_not_in
395
-
364
+
396
365
  result = await make_sienge_bulk_request("GET", "/income", params=params)
397
-
366
+
398
367
  if result["success"]:
399
368
  data = result["data"]
400
369
  income_data = data.get("data", []) if isinstance(data, dict) else data
401
-
370
+
402
371
  return {
403
372
  "success": True,
404
373
  "message": f"✅ Encontradas {len(income_data)} parcelas a receber",
@@ -406,65 +375,70 @@ async def get_sienge_accounts_receivable(start_date: str, end_date: str, selecti
406
375
  "count": len(income_data),
407
376
  "period": f"{start_date} a {end_date}",
408
377
  "selection_type": selection_type,
409
- "filters": params
378
+ "filters": params,
410
379
  }
411
-
380
+
412
381
  return {
413
382
  "success": False,
414
383
  "message": "❌ Erro ao buscar parcelas a receber",
415
384
  "error": result.get("error"),
416
- "details": result.get("message")
385
+ "details": result.get("message"),
417
386
  }
418
387
 
388
+
419
389
  @mcp.tool
420
- async def get_sienge_accounts_receivable_by_bills(bills_ids: List[int], correction_indexer_id: Optional[int] = None,
421
- correction_date: Optional[str] = None) -> Dict:
390
+ async def get_sienge_accounts_receivable_by_bills(
391
+ bills_ids: List[int], correction_indexer_id: Optional[int] = None, correction_date: Optional[str] = None
392
+ ) -> Dict:
422
393
  """
423
394
  Consulta parcelas dos títulos informados via API bulk-data
424
-
395
+
425
396
  Args:
426
397
  bills_ids: Lista de códigos dos títulos - OBRIGATÓRIO
427
398
  correction_indexer_id: Código do indexador de correção
428
399
  correction_date: Data para correção do indexador (YYYY-MM-DD)
429
400
  """
430
- params = {
431
- "billsIds": bills_ids
432
- }
433
-
401
+ params = {"billsIds": bills_ids}
402
+
434
403
  if correction_indexer_id:
435
404
  params["correctionIndexerId"] = correction_indexer_id
436
405
  if correction_date:
437
406
  params["correctionDate"] = correction_date
438
-
407
+
439
408
  result = await make_sienge_bulk_request("GET", "/income/by-bills", params=params)
440
-
409
+
441
410
  if result["success"]:
442
411
  data = result["data"]
443
412
  income_data = data.get("data", []) if isinstance(data, dict) else data
444
-
413
+
445
414
  return {
446
415
  "success": True,
447
416
  "message": f"✅ Encontradas {len(income_data)} parcelas dos títulos informados",
448
417
  "income_data": income_data,
449
418
  "count": len(income_data),
450
419
  "bills_consulted": bills_ids,
451
- "filters": params
420
+ "filters": params,
452
421
  }
453
-
422
+
454
423
  return {
455
424
  "success": False,
456
425
  "message": "❌ Erro ao buscar parcelas dos títulos informados",
457
426
  "error": result.get("error"),
458
- "details": result.get("message")
427
+ "details": result.get("message"),
459
428
  }
460
429
 
430
+
461
431
  @mcp.tool
462
- async def get_sienge_bills(start_date: Optional[str] = None, end_date: Optional[str] = None,
463
- creditor_id: Optional[str] = None, status: Optional[str] = None,
464
- limit: Optional[int] = 50) -> Dict:
432
+ async def get_sienge_bills(
433
+ start_date: Optional[str] = None,
434
+ end_date: Optional[str] = None,
435
+ creditor_id: Optional[str] = None,
436
+ status: Optional[str] = None,
437
+ limit: Optional[int] = 50,
438
+ ) -> Dict:
465
439
  """
466
440
  Consulta títulos a pagar (contas a pagar) - REQUER startDate obrigatório
467
-
441
+
468
442
  Args:
469
443
  start_date: Data inicial obrigatória (YYYY-MM-DD) - padrão últimos 30 dias
470
444
  end_date: Data final (YYYY-MM-DD) - padrão hoje
@@ -473,36 +447,32 @@ async def get_sienge_bills(start_date: Optional[str] = None, end_date: Optional[
473
447
  limit: Máximo de registros (padrão: 50, máx: 200)
474
448
  """
475
449
  from datetime import datetime, timedelta
476
-
450
+
477
451
  # Se start_date não fornecido, usar últimos 30 dias
478
452
  if not start_date:
479
- start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
480
-
453
+ start_date = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
454
+
481
455
  # Se end_date não fornecido, usar hoje
482
456
  if not end_date:
483
- end_date = datetime.now().strftime('%Y-%m-%d')
484
-
457
+ end_date = datetime.now().strftime("%Y-%m-%d")
458
+
485
459
  # Parâmetros obrigatórios
486
- params = {
487
- "startDate": start_date, # OBRIGATÓRIO pela API
488
- "endDate": end_date,
489
- "limit": min(limit or 50, 200)
490
- }
491
-
460
+ params = {"startDate": start_date, "endDate": end_date, "limit": min(limit or 50, 200)} # OBRIGATÓRIO pela API
461
+
492
462
  # Parâmetros opcionais
493
463
  if creditor_id:
494
464
  params["creditor_id"] = creditor_id
495
465
  if status:
496
466
  params["status"] = status
497
-
467
+
498
468
  result = await make_sienge_request("GET", "/bills", params=params)
499
-
469
+
500
470
  if result["success"]:
501
471
  data = result["data"]
502
472
  bills = data.get("results", []) if isinstance(data, dict) else data
503
473
  metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
504
474
  total_count = metadata.get("count", len(bills))
505
-
475
+
506
476
  return {
507
477
  "success": True,
508
478
  "message": f"✅ Encontrados {len(bills)} títulos a pagar (total: {total_count}) - período: {start_date} a {end_date}",
@@ -510,24 +480,30 @@ async def get_sienge_bills(start_date: Optional[str] = None, end_date: Optional[
510
480
  "count": len(bills),
511
481
  "total_count": total_count,
512
482
  "period": {"start_date": start_date, "end_date": end_date},
513
- "filters": params
483
+ "filters": params,
514
484
  }
515
-
485
+
516
486
  return {
517
487
  "success": False,
518
488
  "message": "❌ Erro ao buscar títulos a pagar",
519
489
  "error": result.get("error"),
520
- "details": result.get("message")
490
+ "details": result.get("message"),
521
491
  }
522
492
 
493
+
523
494
  # ============ COMPRAS ============
524
495
 
496
+
525
497
  @mcp.tool
526
- async def get_sienge_purchase_orders(purchase_order_id: Optional[str] = None, status: Optional[str] = None,
527
- date_from: Optional[str] = None, limit: Optional[int] = 50) -> Dict:
498
+ async def get_sienge_purchase_orders(
499
+ purchase_order_id: Optional[str] = None,
500
+ status: Optional[str] = None,
501
+ date_from: Optional[str] = None,
502
+ limit: Optional[int] = 50,
503
+ ) -> Dict:
528
504
  """
529
505
  Consulta pedidos de compra
530
-
506
+
531
507
  Args:
532
508
  purchase_order_id: ID específico do pedido
533
509
  status: Status do pedido
@@ -537,73 +513,71 @@ async def get_sienge_purchase_orders(purchase_order_id: Optional[str] = None, st
537
513
  if purchase_order_id:
538
514
  result = await make_sienge_request("GET", f"/purchase-orders/{purchase_order_id}")
539
515
  if result["success"]:
540
- return {
541
- "success": True,
542
- "message": f"✅ Pedido {purchase_order_id} encontrado",
543
- "purchase_order": result["data"]
544
- }
516
+ return {"success": True, "message": f"✅ Pedido {purchase_order_id} encontrado", "purchase_order": result["data"]}
545
517
  return result
546
-
518
+
547
519
  params = {"limit": min(limit or 50, 200)}
548
520
  if status:
549
521
  params["status"] = status
550
522
  if date_from:
551
523
  params["date_from"] = date_from
552
-
524
+
553
525
  result = await make_sienge_request("GET", "/purchase-orders", params=params)
554
-
526
+
555
527
  if result["success"]:
556
528
  data = result["data"]
557
529
  orders = data.get("results", []) if isinstance(data, dict) else data
558
-
530
+
559
531
  return {
560
532
  "success": True,
561
533
  "message": f"✅ Encontrados {len(orders)} pedidos de compra",
562
534
  "purchase_orders": orders,
563
- "count": len(orders)
535
+ "count": len(orders),
564
536
  }
565
-
537
+
566
538
  return {
567
539
  "success": False,
568
540
  "message": "❌ Erro ao buscar pedidos de compra",
569
541
  "error": result.get("error"),
570
- "details": result.get("message")
542
+ "details": result.get("message"),
571
543
  }
572
544
 
545
+
573
546
  @mcp.tool
574
547
  async def get_sienge_purchase_order_items(purchase_order_id: str) -> Dict:
575
548
  """
576
549
  Consulta itens de um pedido de compra específico
577
-
550
+
578
551
  Args:
579
552
  purchase_order_id: ID do pedido (obrigatório)
580
553
  """
581
554
  result = await make_sienge_request("GET", f"/purchase-orders/{purchase_order_id}/items")
582
-
555
+
583
556
  if result["success"]:
584
557
  data = result["data"]
585
558
  items = data.get("results", []) if isinstance(data, dict) else data
586
-
559
+
587
560
  return {
588
561
  "success": True,
589
562
  "message": f"✅ Encontrados {len(items)} itens no pedido {purchase_order_id}",
590
563
  "purchase_order_id": purchase_order_id,
591
564
  "items": items,
592
- "count": len(items)
565
+ "count": len(items),
593
566
  }
594
-
567
+
595
568
  return {
596
569
  "success": False,
597
570
  "message": f"❌ Erro ao buscar itens do pedido {purchase_order_id}",
598
571
  "error": result.get("error"),
599
- "details": result.get("message")
572
+ "details": result.get("message"),
600
573
  }
601
574
 
575
+
602
576
  @mcp.tool
603
577
  async def get_sienge_purchase_requests(purchase_request_id: Optional[str] = None, limit: Optional[int] = 50) -> Dict:
604
578
  """
605
579
  Consulta solicitações de compra
606
-
580
+
607
581
  Args:
608
582
  purchase_request_id: ID específico da solicitação
609
583
  limit: Máximo de registros
@@ -614,36 +588,37 @@ async def get_sienge_purchase_requests(purchase_request_id: Optional[str] = None
614
588
  return {
615
589
  "success": True,
616
590
  "message": f"✅ Solicitação {purchase_request_id} encontrada",
617
- "purchase_request": result["data"]
591
+ "purchase_request": result["data"],
618
592
  }
619
593
  return result
620
-
594
+
621
595
  params = {"limit": min(limit or 50, 200)}
622
596
  result = await make_sienge_request("GET", "/purchase-requests", params=params)
623
-
597
+
624
598
  if result["success"]:
625
599
  data = result["data"]
626
600
  requests = data.get("results", []) if isinstance(data, dict) else data
627
-
601
+
628
602
  return {
629
603
  "success": True,
630
604
  "message": f"✅ Encontradas {len(requests)} solicitações de compra",
631
605
  "purchase_requests": requests,
632
- "count": len(requests)
606
+ "count": len(requests),
633
607
  }
634
-
608
+
635
609
  return {
636
610
  "success": False,
637
611
  "message": "❌ Erro ao buscar solicitações de compra",
638
612
  "error": result.get("error"),
639
- "details": result.get("message")
613
+ "details": result.get("message"),
640
614
  }
641
615
 
616
+
642
617
  @mcp.tool
643
618
  async def create_sienge_purchase_request(description: str, project_id: str, items: List[Dict[str, Any]]) -> Dict:
644
619
  """
645
620
  Cria nova solicitação de compra
646
-
621
+
647
622
  Args:
648
623
  description: Descrição da solicitação
649
624
  project_id: ID do projeto/obra
@@ -653,89 +628,97 @@ async def create_sienge_purchase_request(description: str, project_id: str, item
653
628
  "description": description,
654
629
  "project_id": project_id,
655
630
  "items": items,
656
- "date": datetime.now().strftime("%Y-%m-%d")
631
+ "date": datetime.now().strftime("%Y-%m-%d"),
657
632
  }
658
-
633
+
659
634
  result = await make_sienge_request("POST", "/purchase-requests", json_data=request_data)
660
-
635
+
661
636
  if result["success"]:
662
637
  return {
663
638
  "success": True,
664
639
  "message": "✅ Solicitação de compra criada com sucesso",
665
640
  "request_id": result["data"].get("id"),
666
- "data": result["data"]
641
+ "data": result["data"],
667
642
  }
668
-
643
+
669
644
  return {
670
645
  "success": False,
671
646
  "message": "❌ Erro ao criar solicitação de compra",
672
647
  "error": result.get("error"),
673
- "details": result.get("message")
648
+ "details": result.get("message"),
674
649
  }
675
650
 
651
+
676
652
  # ============ NOTAS FISCAIS DE COMPRA ============
677
653
 
654
+
678
655
  @mcp.tool
679
656
  async def get_sienge_purchase_invoice(sequential_number: int) -> Dict:
680
657
  """
681
658
  Consulta nota fiscal de compra por número sequencial
682
-
659
+
683
660
  Args:
684
661
  sequential_number: Número sequencial da nota fiscal
685
662
  """
686
663
  result = await make_sienge_request("GET", f"/purchase-invoices/{sequential_number}")
687
-
664
+
688
665
  if result["success"]:
689
- return {
690
- "success": True,
691
- "message": f"✅ Nota fiscal {sequential_number} encontrada",
692
- "invoice": result["data"]
693
- }
694
-
666
+ return {"success": True, "message": f"✅ Nota fiscal {sequential_number} encontrada", "invoice": result["data"]}
667
+
695
668
  return {
696
669
  "success": False,
697
670
  "message": f"❌ Erro ao buscar nota fiscal {sequential_number}",
698
671
  "error": result.get("error"),
699
- "details": result.get("message")
672
+ "details": result.get("message"),
700
673
  }
701
674
 
675
+
702
676
  @mcp.tool
703
677
  async def get_sienge_purchase_invoice_items(sequential_number: int) -> Dict:
704
678
  """
705
679
  Consulta itens de uma nota fiscal de compra
706
-
680
+
707
681
  Args:
708
682
  sequential_number: Número sequencial da nota fiscal
709
683
  """
710
684
  result = await make_sienge_request("GET", f"/purchase-invoices/{sequential_number}/items")
711
-
685
+
712
686
  if result["success"]:
713
687
  data = result["data"]
714
688
  items = data.get("results", []) if isinstance(data, dict) else data
715
689
  metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
716
-
690
+
717
691
  return {
718
692
  "success": True,
719
693
  "message": f"✅ Encontrados {len(items)} itens na nota fiscal {sequential_number}",
720
694
  "items": items,
721
695
  "count": len(items),
722
- "metadata": metadata
696
+ "metadata": metadata,
723
697
  }
724
-
698
+
725
699
  return {
726
700
  "success": False,
727
701
  "message": f"❌ Erro ao buscar itens da nota fiscal {sequential_number}",
728
702
  "error": result.get("error"),
729
- "details": result.get("message")
703
+ "details": result.get("message"),
730
704
  }
731
705
 
706
+
732
707
  @mcp.tool
733
- async def create_sienge_purchase_invoice(document_id: str, number: str, supplier_id: int, company_id: int,
734
- movement_type_id: int, movement_date: str, issue_date: str,
735
- series: Optional[str] = None, notes: Optional[str] = None) -> Dict:
708
+ async def create_sienge_purchase_invoice(
709
+ document_id: str,
710
+ number: str,
711
+ supplier_id: int,
712
+ company_id: int,
713
+ movement_type_id: int,
714
+ movement_date: str,
715
+ issue_date: str,
716
+ series: Optional[str] = None,
717
+ notes: Optional[str] = None,
718
+ ) -> Dict:
736
719
  """
737
720
  Cadastra uma nova nota fiscal de compra
738
-
721
+
739
722
  Args:
740
723
  document_id: ID do documento (ex: "NF")
741
724
  number: Número da nota fiscal
@@ -754,37 +737,38 @@ async def create_sienge_purchase_invoice(document_id: str, number: str, supplier
754
737
  "companyId": company_id,
755
738
  "movementTypeId": movement_type_id,
756
739
  "movementDate": movement_date,
757
- "issueDate": issue_date
740
+ "issueDate": issue_date,
758
741
  }
759
-
742
+
760
743
  if series:
761
744
  invoice_data["series"] = series
762
745
  if notes:
763
746
  invoice_data["notes"] = notes
764
-
747
+
765
748
  result = await make_sienge_request("POST", "/purchase-invoices", json_data=invoice_data)
766
-
749
+
767
750
  if result["success"]:
768
- return {
769
- "success": True,
770
- "message": f"✅ Nota fiscal {number} criada com sucesso",
771
- "invoice": result["data"]
772
- }
773
-
751
+ return {"success": True, "message": f"✅ Nota fiscal {number} criada com sucesso", "invoice": result["data"]}
752
+
774
753
  return {
775
754
  "success": False,
776
755
  "message": f"❌ Erro ao criar nota fiscal {number}",
777
756
  "error": result.get("error"),
778
- "details": result.get("message")
757
+ "details": result.get("message"),
779
758
  }
780
759
 
760
+
781
761
  @mcp.tool
782
- async def add_items_to_purchase_invoice(sequential_number: int, deliveries_order: List[Dict[str, Any]],
783
- copy_notes_purchase_orders: bool = True, copy_notes_resources: bool = False,
784
- copy_attachments_purchase_orders: bool = True) -> Dict:
762
+ async def add_items_to_purchase_invoice(
763
+ sequential_number: int,
764
+ deliveries_order: List[Dict[str, Any]],
765
+ copy_notes_purchase_orders: bool = True,
766
+ copy_notes_resources: bool = False,
767
+ copy_attachments_purchase_orders: bool = True,
768
+ ) -> Dict:
785
769
  """
786
770
  Insere itens em uma nota fiscal a partir de entregas de pedidos de compra
787
-
771
+
788
772
  Args:
789
773
  sequential_number: Número sequencial da nota fiscal
790
774
  deliveries_order: Lista de entregas com estrutura:
@@ -801,33 +785,41 @@ async def add_items_to_purchase_invoice(sequential_number: int, deliveries_order
801
785
  "deliveriesOrder": deliveries_order,
802
786
  "copyNotesPurchaseOrders": copy_notes_purchase_orders,
803
787
  "copyNotesResources": copy_notes_resources,
804
- "copyAttachmentsPurchaseOrders": copy_attachments_purchase_orders
788
+ "copyAttachmentsPurchaseOrders": copy_attachments_purchase_orders,
805
789
  }
806
-
807
- result = await make_sienge_request("POST", f"/purchase-invoices/{sequential_number}/items/purchase-orders/delivery-schedules", json_data=item_data)
808
-
790
+
791
+ result = await make_sienge_request(
792
+ "POST", f"/purchase-invoices/{sequential_number}/items/purchase-orders/delivery-schedules", json_data=item_data
793
+ )
794
+
809
795
  if result["success"]:
810
796
  return {
811
797
  "success": True,
812
798
  "message": f"✅ Itens adicionados à nota fiscal {sequential_number} com sucesso",
813
- "item": result["data"]
799
+ "item": result["data"],
814
800
  }
815
-
801
+
816
802
  return {
817
803
  "success": False,
818
804
  "message": f"❌ Erro ao adicionar itens à nota fiscal {sequential_number}",
819
805
  "error": result.get("error"),
820
- "details": result.get("message")
806
+ "details": result.get("message"),
821
807
  }
822
808
 
809
+
823
810
  @mcp.tool
824
- async def get_sienge_purchase_invoices_deliveries_attended(bill_id: Optional[int] = None, sequential_number: Optional[int] = None,
825
- purchase_order_id: Optional[int] = None, invoice_item_number: Optional[int] = None,
826
- purchase_order_item_number: Optional[int] = None,
827
- limit: Optional[int] = 100, offset: Optional[int] = 0) -> Dict:
811
+ async def get_sienge_purchase_invoices_deliveries_attended(
812
+ bill_id: Optional[int] = None,
813
+ sequential_number: Optional[int] = None,
814
+ purchase_order_id: Optional[int] = None,
815
+ invoice_item_number: Optional[int] = None,
816
+ purchase_order_item_number: Optional[int] = None,
817
+ limit: Optional[int] = 100,
818
+ offset: Optional[int] = 0,
819
+ ) -> Dict:
828
820
  """
829
821
  Lista entregas atendidas entre pedidos de compra e notas fiscais
830
-
822
+
831
823
  Args:
832
824
  bill_id: ID do título da nota fiscal
833
825
  sequential_number: Número sequencial da nota fiscal
@@ -838,7 +830,7 @@ async def get_sienge_purchase_invoices_deliveries_attended(bill_id: Optional[int
838
830
  offset: Deslocamento (padrão: 0)
839
831
  """
840
832
  params = {"limit": min(limit or 100, 200), "offset": offset or 0}
841
-
833
+
842
834
  if bill_id:
843
835
  params["billId"] = bill_id
844
836
  if sequential_number:
@@ -849,37 +841,39 @@ async def get_sienge_purchase_invoices_deliveries_attended(bill_id: Optional[int
849
841
  params["invoiceItemNumber"] = invoice_item_number
850
842
  if purchase_order_item_number:
851
843
  params["purchaseOrderItemNumber"] = purchase_order_item_number
852
-
844
+
853
845
  result = await make_sienge_request("GET", "/purchase-invoices/deliveries-attended", params=params)
854
-
846
+
855
847
  if result["success"]:
856
848
  data = result["data"]
857
849
  deliveries = data.get("results", []) if isinstance(data, dict) else data
858
850
  metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
859
-
851
+
860
852
  return {
861
853
  "success": True,
862
854
  "message": f"✅ Encontradas {len(deliveries)} entregas atendidas",
863
855
  "deliveries": deliveries,
864
856
  "count": len(deliveries),
865
857
  "metadata": metadata,
866
- "filters": params
858
+ "filters": params,
867
859
  }
868
-
860
+
869
861
  return {
870
862
  "success": False,
871
863
  "message": "❌ Erro ao buscar entregas atendidas",
872
864
  "error": result.get("error"),
873
- "details": result.get("message")
865
+ "details": result.get("message"),
874
866
  }
875
867
 
868
+
876
869
  # ============ ESTOQUE ============
877
870
 
871
+
878
872
  @mcp.tool
879
873
  async def get_sienge_stock_inventory(cost_center_id: str, resource_id: Optional[str] = None) -> Dict:
880
874
  """
881
875
  Consulta inventário de estoque por centro de custo
882
-
876
+
883
877
  Args:
884
878
  cost_center_id: ID do centro de custo (obrigatório)
885
879
  resource_id: ID do insumo específico (opcional)
@@ -888,67 +882,75 @@ async def get_sienge_stock_inventory(cost_center_id: str, resource_id: Optional[
888
882
  endpoint = f"/stock-inventories/{cost_center_id}/items/{resource_id}"
889
883
  else:
890
884
  endpoint = f"/stock-inventories/{cost_center_id}/items"
891
-
885
+
892
886
  result = await make_sienge_request("GET", endpoint)
893
-
887
+
894
888
  if result["success"]:
895
889
  data = result["data"]
896
890
  items = data.get("results", []) if isinstance(data, dict) else data
897
891
  count = len(items) if isinstance(items, list) else 1
898
-
892
+
899
893
  return {
900
894
  "success": True,
901
895
  "message": f"✅ Inventário do centro de custo {cost_center_id}",
902
896
  "cost_center_id": cost_center_id,
903
897
  "inventory": items,
904
- "count": count
898
+ "count": count,
905
899
  }
906
-
900
+
907
901
  return {
908
902
  "success": False,
909
903
  "message": f"❌ Erro ao consultar estoque do centro {cost_center_id}",
910
904
  "error": result.get("error"),
911
- "details": result.get("message")
905
+ "details": result.get("message"),
912
906
  }
913
907
 
908
+
914
909
  @mcp.tool
915
910
  async def get_sienge_stock_reservations(limit: Optional[int] = 50) -> Dict:
916
911
  """
917
912
  Lista reservas de estoque
918
-
913
+
919
914
  Args:
920
915
  limit: Máximo de registros
921
916
  """
922
917
  params = {"limit": min(limit or 50, 200)}
923
918
  result = await make_sienge_request("GET", "/stock-reservations", params=params)
924
-
919
+
925
920
  if result["success"]:
926
921
  data = result["data"]
927
922
  reservations = data.get("results", []) if isinstance(data, dict) else data
928
-
923
+
929
924
  return {
930
925
  "success": True,
931
926
  "message": f"✅ Encontradas {len(reservations)} reservas de estoque",
932
927
  "reservations": reservations,
933
- "count": len(reservations)
928
+ "count": len(reservations),
934
929
  }
935
-
930
+
936
931
  return {
937
932
  "success": False,
938
933
  "message": "❌ Erro ao buscar reservas de estoque",
939
934
  "error": result.get("error"),
940
- "details": result.get("message")
935
+ "details": result.get("message"),
941
936
  }
942
937
 
938
+
943
939
  # ============ PROJETOS/OBRAS ============
944
940
 
941
+
945
942
  @mcp.tool
946
- async def get_sienge_projects(limit: Optional[int] = 100, offset: Optional[int] = 0, company_id: Optional[int] = None,
947
- enterprise_type: Optional[int] = None, receivable_register: Optional[str] = None,
948
- only_buildings_enabled: Optional[bool] = False) -> Dict:
943
+ async def get_sienge_projects(
944
+ limit: Optional[int] = 100,
945
+ offset: Optional[int] = 0,
946
+ company_id: Optional[int] = None,
947
+ enterprise_type: Optional[int] = None,
948
+ receivable_register: Optional[str] = None,
949
+ only_buildings_enabled: Optional[bool] = False,
950
+ ) -> Dict:
949
951
  """
950
952
  Busca empreendimentos/obras no Sienge
951
-
953
+
952
954
  Args:
953
955
  limit: Máximo de registros (padrão: 100, máximo: 200)
954
956
  offset: Pular registros (padrão: 0)
@@ -958,7 +960,7 @@ async def get_sienge_projects(limit: Optional[int] = 100, offset: Optional[int]
958
960
  only_buildings_enabled: Retornar apenas obras habilitadas para integração orçamentária
959
961
  """
960
962
  params = {"limit": min(limit or 100, 200), "offset": offset or 0}
961
-
963
+
962
964
  if company_id:
963
965
  params["companyId"] = company_id
964
966
  if enterprise_type:
@@ -967,120 +969,125 @@ async def get_sienge_projects(limit: Optional[int] = 100, offset: Optional[int]
967
969
  params["receivableRegister"] = receivable_register
968
970
  if only_buildings_enabled:
969
971
  params["onlyBuildingsEnabledForIntegration"] = only_buildings_enabled
970
-
972
+
971
973
  result = await make_sienge_request("GET", "/enterprises", params=params)
972
-
974
+
973
975
  if result["success"]:
974
976
  data = result["data"]
975
977
  enterprises = data.get("results", []) if isinstance(data, dict) else data
976
978
  metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
977
-
979
+
978
980
  return {
979
981
  "success": True,
980
982
  "message": f"✅ Encontrados {len(enterprises)} empreendimentos",
981
983
  "enterprises": enterprises,
982
984
  "count": len(enterprises),
983
985
  "metadata": metadata,
984
- "filters": params
986
+ "filters": params,
985
987
  }
986
-
988
+
987
989
  return {
988
990
  "success": False,
989
991
  "message": "❌ Erro ao buscar empreendimentos",
990
992
  "error": result.get("error"),
991
- "details": result.get("message")
993
+ "details": result.get("message"),
992
994
  }
993
995
 
996
+
994
997
  @mcp.tool
995
998
  async def get_sienge_enterprise_by_id(enterprise_id: int) -> Dict:
996
999
  """
997
1000
  Busca um empreendimento específico por ID no Sienge
998
-
1001
+
999
1002
  Args:
1000
1003
  enterprise_id: ID do empreendimento
1001
1004
  """
1002
1005
  result = await make_sienge_request("GET", f"/enterprises/{enterprise_id}")
1003
-
1006
+
1004
1007
  if result["success"]:
1005
- return {
1006
- "success": True,
1007
- "message": f"✅ Empreendimento {enterprise_id} encontrado",
1008
- "enterprise": result["data"]
1009
- }
1010
-
1008
+ return {"success": True, "message": f"✅ Empreendimento {enterprise_id} encontrado", "enterprise": result["data"]}
1009
+
1011
1010
  return {
1012
1011
  "success": False,
1013
1012
  "message": f"❌ Erro ao buscar empreendimento {enterprise_id}",
1014
1013
  "error": result.get("error"),
1015
- "details": result.get("message")
1014
+ "details": result.get("message"),
1016
1015
  }
1017
1016
 
1017
+
1018
1018
  @mcp.tool
1019
1019
  async def get_sienge_enterprise_groupings(enterprise_id: int) -> Dict:
1020
1020
  """
1021
1021
  Busca agrupamentos de unidades de um empreendimento específico
1022
-
1022
+
1023
1023
  Args:
1024
1024
  enterprise_id: ID do empreendimento
1025
1025
  """
1026
1026
  result = await make_sienge_request("GET", f"/enterprises/{enterprise_id}/groupings")
1027
-
1027
+
1028
1028
  if result["success"]:
1029
1029
  groupings = result["data"]
1030
1030
  return {
1031
1031
  "success": True,
1032
1032
  "message": f"✅ Agrupamentos do empreendimento {enterprise_id} encontrados",
1033
1033
  "groupings": groupings,
1034
- "count": len(groupings) if isinstance(groupings, list) else 0
1034
+ "count": len(groupings) if isinstance(groupings, list) else 0,
1035
1035
  }
1036
-
1036
+
1037
1037
  return {
1038
1038
  "success": False,
1039
1039
  "message": f"❌ Erro ao buscar agrupamentos do empreendimento {enterprise_id}",
1040
1040
  "error": result.get("error"),
1041
- "details": result.get("message")
1041
+ "details": result.get("message"),
1042
1042
  }
1043
1043
 
1044
+
1044
1045
  @mcp.tool
1045
1046
  async def get_sienge_units(limit: Optional[int] = 50, offset: Optional[int] = 0) -> Dict:
1046
1047
  """
1047
1048
  Consulta unidades cadastradas no Sienge
1048
-
1049
+
1049
1050
  Args:
1050
1051
  limit: Máximo de registros (padrão: 50)
1051
1052
  offset: Pular registros (padrão: 0)
1052
1053
  """
1053
1054
  params = {"limit": min(limit or 50, 200), "offset": offset or 0}
1054
1055
  result = await make_sienge_request("GET", "/units", params=params)
1055
-
1056
+
1056
1057
  if result["success"]:
1057
1058
  data = result["data"]
1058
1059
  units = data.get("results", []) if isinstance(data, dict) else data
1059
1060
  metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
1060
1061
  total_count = metadata.get("count", len(units))
1061
-
1062
+
1062
1063
  return {
1063
1064
  "success": True,
1064
1065
  "message": f"✅ Encontradas {len(units)} unidades (total: {total_count})",
1065
1066
  "units": units,
1066
- "count": len(units)
1067
+ "count": len(units),
1067
1068
  }
1068
-
1069
+
1069
1070
  return {
1070
1071
  "success": False,
1071
1072
  "message": "❌ Erro ao buscar unidades",
1072
1073
  "error": result.get("error"),
1073
- "details": result.get("message")
1074
+ "details": result.get("message"),
1074
1075
  }
1075
1076
 
1077
+
1076
1078
  # ============ CUSTOS ============
1077
1079
 
1080
+
1078
1081
  @mcp.tool
1079
- async def get_sienge_unit_cost_tables(table_code: Optional[str] = None, description: Optional[str] = None,
1080
- status: Optional[str] = "Active", integration_enabled: Optional[bool] = None) -> Dict:
1082
+ async def get_sienge_unit_cost_tables(
1083
+ table_code: Optional[str] = None,
1084
+ description: Optional[str] = None,
1085
+ status: Optional[str] = "Active",
1086
+ integration_enabled: Optional[bool] = None,
1087
+ ) -> Dict:
1081
1088
  """
1082
1089
  Consulta tabelas de custos unitários
1083
-
1090
+
1084
1091
  Args:
1085
1092
  table_code: Código da tabela (opcional)
1086
1093
  description: Descrição da tabela (opcional)
@@ -1088,80 +1095,684 @@ async def get_sienge_unit_cost_tables(table_code: Optional[str] = None, descript
1088
1095
  integration_enabled: Se habilitada para integração
1089
1096
  """
1090
1097
  params = {"status": status or "Active"}
1091
-
1098
+
1092
1099
  if table_code:
1093
1100
  params["table_code"] = table_code
1094
1101
  if description:
1095
1102
  params["description"] = description
1096
1103
  if integration_enabled is not None:
1097
1104
  params["integration_enabled"] = integration_enabled
1098
-
1105
+
1099
1106
  result = await make_sienge_request("GET", "/unit-cost-tables", params=params)
1100
-
1107
+
1101
1108
  if result["success"]:
1102
1109
  data = result["data"]
1103
1110
  tables = data.get("results", []) if isinstance(data, dict) else data
1104
-
1111
+
1105
1112
  return {
1106
1113
  "success": True,
1107
1114
  "message": f"✅ Encontradas {len(tables)} tabelas de custos",
1108
1115
  "cost_tables": tables,
1109
- "count": len(tables)
1116
+ "count": len(tables),
1110
1117
  }
1111
-
1118
+
1112
1119
  return {
1113
1120
  "success": False,
1114
1121
  "message": "❌ Erro ao buscar tabelas de custos",
1115
1122
  "error": result.get("error"),
1116
- "details": result.get("message")
1123
+ "details": result.get("message"),
1117
1124
  }
1118
1125
 
1126
+
1127
+ # ============ SEARCH UNIVERSAL (COMPATIBILIDADE CHATGPT) ============
1128
+
1129
+
1130
+ @mcp.tool
1131
+ async def search_sienge_data(
1132
+ query: str,
1133
+ entity_type: Optional[str] = None,
1134
+ limit: Optional[int] = 20,
1135
+ filters: Optional[Dict[str, Any]] = None
1136
+ ) -> Dict:
1137
+ """
1138
+ Busca universal no Sienge - compatível com ChatGPT/OpenAI MCP
1139
+
1140
+ Permite buscar em múltiplas entidades do Sienge de forma unificada.
1141
+
1142
+ Args:
1143
+ query: Termo de busca (nome, código, descrição, etc.)
1144
+ entity_type: Tipo de entidade (customers, creditors, projects, bills, purchase_orders, etc.)
1145
+ limit: Máximo de registros (padrão: 20, máximo: 100)
1146
+ filters: Filtros específicos por tipo de entidade
1147
+ """
1148
+ search_results = []
1149
+ limit = min(limit or 20, 100)
1150
+
1151
+ # Se entity_type específico, buscar apenas nele
1152
+ if entity_type:
1153
+ result = await _search_specific_entity(entity_type, query, limit, filters or {})
1154
+ if result["success"]:
1155
+ return result
1156
+ else:
1157
+ return {
1158
+ "success": False,
1159
+ "message": f"❌ Erro na busca em {entity_type}",
1160
+ "error": result.get("error"),
1161
+ "query": query,
1162
+ "entity_type": entity_type
1163
+ }
1164
+
1165
+ # Busca universal em múltiplas entidades
1166
+ entities_to_search = [
1167
+ ("customers", "clientes"),
1168
+ ("creditors", "credores/fornecedores"),
1169
+ ("projects", "empreendimentos/obras"),
1170
+ ("bills", "títulos a pagar"),
1171
+ ("purchase_orders", "pedidos de compra")
1172
+ ]
1173
+
1174
+ total_found = 0
1175
+
1176
+ for entity_key, entity_name in entities_to_search:
1177
+ try:
1178
+ entity_result = await _search_specific_entity(entity_key, query, min(5, limit), {})
1179
+ if entity_result["success"] and entity_result.get("count", 0) > 0:
1180
+ search_results.append({
1181
+ "entity_type": entity_key,
1182
+ "entity_name": entity_name,
1183
+ "count": entity_result["count"],
1184
+ "results": entity_result["data"][:5], # Limitar a 5 por entidade na busca universal
1185
+ "has_more": entity_result["count"] > 5
1186
+ })
1187
+ total_found += entity_result["count"]
1188
+ except Exception as e:
1189
+ # Continuar com outras entidades se uma falhar
1190
+ continue
1191
+
1192
+ if search_results:
1193
+ return {
1194
+ "success": True,
1195
+ "message": f"✅ Busca '{query}' encontrou resultados em {len(search_results)} entidades (total: {total_found} registros)",
1196
+ "query": query,
1197
+ "total_entities": len(search_results),
1198
+ "total_records": total_found,
1199
+ "results_by_entity": search_results,
1200
+ "suggestion": "Use entity_type para buscar especificamente em uma entidade e obter mais resultados"
1201
+ }
1202
+ else:
1203
+ return {
1204
+ "success": False,
1205
+ "message": f"❌ Nenhum resultado encontrado para '{query}'",
1206
+ "query": query,
1207
+ "searched_entities": [name for _, name in entities_to_search],
1208
+ "suggestion": "Tente termos mais específicos ou use os tools específicos de cada entidade"
1209
+ }
1210
+
1211
+
1212
+ async def _search_specific_entity(entity_type: str, query: str, limit: int, filters: Dict) -> Dict:
1213
+ """Função auxiliar para buscar em uma entidade específica"""
1214
+
1215
+ if entity_type == "customers":
1216
+ result = await get_sienge_customers(limit=limit, search=query)
1217
+ if result["success"]:
1218
+ return {
1219
+ "success": True,
1220
+ "data": result["customers"],
1221
+ "count": result["count"],
1222
+ "entity_type": "customers"
1223
+ }
1224
+
1225
+ elif entity_type == "creditors":
1226
+ result = await get_sienge_creditors(limit=limit, search=query)
1227
+ if result["success"]:
1228
+ return {
1229
+ "success": True,
1230
+ "data": result["creditors"],
1231
+ "count": result["count"],
1232
+ "entity_type": "creditors"
1233
+ }
1234
+
1235
+ elif entity_type == "projects" or entity_type == "enterprises":
1236
+ # Para projetos, usar filtros mais específicos se disponível
1237
+ company_id = filters.get("company_id")
1238
+ result = await get_sienge_projects(limit=limit, company_id=company_id)
1239
+ if result["success"]:
1240
+ # Filtrar por query se fornecida
1241
+ projects = result["enterprises"]
1242
+ if query:
1243
+ projects = [
1244
+ p for p in projects
1245
+ if query.lower() in str(p.get("description", "")).lower()
1246
+ or query.lower() in str(p.get("name", "")).lower()
1247
+ or query.lower() in str(p.get("code", "")).lower()
1248
+ ]
1249
+ return {
1250
+ "success": True,
1251
+ "data": projects,
1252
+ "count": len(projects),
1253
+ "entity_type": "projects"
1254
+ }
1255
+
1256
+ elif entity_type == "bills":
1257
+ # Para títulos, usar data padrão se não especificada
1258
+ start_date = filters.get("start_date")
1259
+ end_date = filters.get("end_date")
1260
+ result = await get_sienge_bills(
1261
+ start_date=start_date,
1262
+ end_date=end_date,
1263
+ limit=limit
1264
+ )
1265
+ if result["success"]:
1266
+ return {
1267
+ "success": True,
1268
+ "data": result["bills"],
1269
+ "count": result["count"],
1270
+ "entity_type": "bills"
1271
+ }
1272
+
1273
+ elif entity_type == "purchase_orders":
1274
+ result = await get_sienge_purchase_orders(limit=limit)
1275
+ if result["success"]:
1276
+ orders = result["purchase_orders"]
1277
+ # Filtrar por query se fornecida
1278
+ if query:
1279
+ orders = [
1280
+ o for o in orders
1281
+ if query.lower() in str(o.get("description", "")).lower()
1282
+ or query.lower() in str(o.get("id", "")).lower()
1283
+ ]
1284
+ return {
1285
+ "success": True,
1286
+ "data": orders,
1287
+ "count": len(orders),
1288
+ "entity_type": "purchase_orders"
1289
+ }
1290
+
1291
+ # Se chegou aqui, entidade não suportada ou erro
1292
+ return {
1293
+ "success": False,
1294
+ "error": f"Entidade '{entity_type}' não suportada ou erro na busca",
1295
+ "supported_entities": ["customers", "creditors", "projects", "bills", "purchase_orders"]
1296
+ }
1297
+
1298
+
1299
+ @mcp.tool
1300
+ async def list_sienge_entities() -> Dict:
1301
+ """
1302
+ Lista todas as entidades disponíveis no Sienge MCP para busca
1303
+
1304
+ Retorna informações sobre os tipos de dados que podem ser consultados
1305
+ """
1306
+ entities = [
1307
+ {
1308
+ "type": "customers",
1309
+ "name": "Clientes",
1310
+ "description": "Clientes cadastrados no sistema",
1311
+ "search_fields": ["nome", "documento", "email"],
1312
+ "tools": ["get_sienge_customers", "search_sienge_data"]
1313
+ },
1314
+ {
1315
+ "type": "creditors",
1316
+ "name": "Credores/Fornecedores",
1317
+ "description": "Fornecedores e credores cadastrados",
1318
+ "search_fields": ["nome", "documento"],
1319
+ "tools": ["get_sienge_creditors", "get_sienge_creditor_bank_info"]
1320
+ },
1321
+ {
1322
+ "type": "projects",
1323
+ "name": "Empreendimentos/Obras",
1324
+ "description": "Projetos e obras cadastrados",
1325
+ "search_fields": ["código", "descrição", "nome"],
1326
+ "tools": ["get_sienge_projects", "get_sienge_enterprise_by_id"]
1327
+ },
1328
+ {
1329
+ "type": "bills",
1330
+ "name": "Títulos a Pagar",
1331
+ "description": "Contas a pagar e títulos financeiros",
1332
+ "search_fields": ["número", "credor", "valor"],
1333
+ "tools": ["get_sienge_bills"]
1334
+ },
1335
+ {
1336
+ "type": "purchase_orders",
1337
+ "name": "Pedidos de Compra",
1338
+ "description": "Pedidos de compra e solicitações",
1339
+ "search_fields": ["id", "descrição", "status"],
1340
+ "tools": ["get_sienge_purchase_orders", "get_sienge_purchase_requests"]
1341
+ },
1342
+ {
1343
+ "type": "invoices",
1344
+ "name": "Notas Fiscais",
1345
+ "description": "Notas fiscais de compra",
1346
+ "search_fields": ["número", "série", "fornecedor"],
1347
+ "tools": ["get_sienge_purchase_invoice"]
1348
+ },
1349
+ {
1350
+ "type": "stock",
1351
+ "name": "Estoque",
1352
+ "description": "Inventário e movimentações de estoque",
1353
+ "search_fields": ["centro_custo", "recurso"],
1354
+ "tools": ["get_sienge_stock_inventory", "get_sienge_stock_reservations"]
1355
+ },
1356
+ {
1357
+ "type": "financial",
1358
+ "name": "Financeiro",
1359
+ "description": "Contas a receber e movimentações financeiras",
1360
+ "search_fields": ["período", "cliente", "valor"],
1361
+ "tools": ["get_sienge_accounts_receivable"]
1362
+ }
1363
+ ]
1364
+
1365
+ return {
1366
+ "success": True,
1367
+ "message": f"✅ {len(entities)} tipos de entidades disponíveis no Sienge",
1368
+ "entities": entities,
1369
+ "total_tools": sum(len(e["tools"]) for e in entities),
1370
+ "usage_example": {
1371
+ "search_all": "search_sienge_data('nome_cliente')",
1372
+ "search_specific": "search_sienge_data('nome_cliente', entity_type='customers')",
1373
+ "direct_access": "get_sienge_customers(search='nome_cliente')"
1374
+ }
1375
+ }
1376
+
1377
+
1378
+ # ============ PAGINATION E NAVEGAÇÃO ============
1379
+
1380
+
1381
+ @mcp.tool
1382
+ async def get_sienge_data_paginated(
1383
+ entity_type: str,
1384
+ page: int = 1,
1385
+ page_size: int = 20,
1386
+ filters: Optional[Dict[str, Any]] = None,
1387
+ sort_by: Optional[str] = None
1388
+ ) -> Dict:
1389
+ """
1390
+ Busca dados do Sienge com paginação avançada - compatível com ChatGPT
1391
+
1392
+ Args:
1393
+ entity_type: Tipo de entidade (customers, creditors, projects, bills, etc.)
1394
+ page: Número da página (começando em 1)
1395
+ page_size: Registros por página (máximo 50)
1396
+ filters: Filtros específicos da entidade
1397
+ sort_by: Campo para ordenação (se suportado)
1398
+ """
1399
+ page_size = min(page_size, 50)
1400
+ offset = (page - 1) * page_size
1401
+
1402
+ filters = filters or {}
1403
+
1404
+ # Mapear para os tools existentes com offset
1405
+ if entity_type == "customers":
1406
+ search = filters.get("search")
1407
+ customer_type_id = filters.get("customer_type_id")
1408
+ result = await get_sienge_customers(
1409
+ limit=page_size,
1410
+ offset=offset,
1411
+ search=search,
1412
+ customer_type_id=customer_type_id
1413
+ )
1414
+
1415
+ elif entity_type == "creditors":
1416
+ search = filters.get("search")
1417
+ result = await get_sienge_creditors(
1418
+ limit=page_size,
1419
+ offset=offset,
1420
+ search=search
1421
+ )
1422
+
1423
+ elif entity_type == "projects":
1424
+ result = await get_sienge_projects(
1425
+ limit=page_size,
1426
+ offset=offset,
1427
+ company_id=filters.get("company_id"),
1428
+ enterprise_type=filters.get("enterprise_type")
1429
+ )
1430
+
1431
+ elif entity_type == "bills":
1432
+ result = await get_sienge_bills(
1433
+ start_date=filters.get("start_date"),
1434
+ end_date=filters.get("end_date"),
1435
+ creditor_id=filters.get("creditor_id"),
1436
+ status=filters.get("status"),
1437
+ limit=page_size
1438
+ )
1439
+
1440
+ else:
1441
+ return {
1442
+ "success": False,
1443
+ "message": f"❌ Tipo de entidade '{entity_type}' não suportado para paginação",
1444
+ "supported_types": ["customers", "creditors", "projects", "bills"]
1445
+ }
1446
+
1447
+ if result["success"]:
1448
+ # Calcular informações de paginação
1449
+ total_count = result.get("total_count", result.get("count", 0))
1450
+ total_pages = (total_count + page_size - 1) // page_size if total_count > 0 else 1
1451
+
1452
+ return {
1453
+ "success": True,
1454
+ "message": f"✅ Página {page} de {total_pages} - {entity_type}",
1455
+ "data": result.get(entity_type, result.get("data", [])),
1456
+ "pagination": {
1457
+ "current_page": page,
1458
+ "page_size": page_size,
1459
+ "total_pages": total_pages,
1460
+ "total_records": total_count,
1461
+ "has_next": page < total_pages,
1462
+ "has_previous": page > 1,
1463
+ "next_page": page + 1 if page < total_pages else None,
1464
+ "previous_page": page - 1 if page > 1 else None
1465
+ },
1466
+ "entity_type": entity_type,
1467
+ "filters_applied": filters
1468
+ }
1469
+
1470
+ return result
1471
+
1472
+
1473
+ @mcp.tool
1474
+ async def search_sienge_financial_data(
1475
+ period_start: str,
1476
+ period_end: str,
1477
+ search_type: str = "both",
1478
+ amount_min: Optional[float] = None,
1479
+ amount_max: Optional[float] = None,
1480
+ customer_creditor_search: Optional[str] = None
1481
+ ) -> Dict:
1482
+ """
1483
+ Busca avançada em dados financeiros do Sienge - Contas a Pagar e Receber
1484
+
1485
+ Args:
1486
+ period_start: Data inicial do período (YYYY-MM-DD)
1487
+ period_end: Data final do período (YYYY-MM-DD)
1488
+ search_type: Tipo de busca ("receivable", "payable", "both")
1489
+ amount_min: Valor mínimo (opcional)
1490
+ amount_max: Valor máximo (opcional)
1491
+ customer_creditor_search: Buscar por nome de cliente/credor (opcional)
1492
+ """
1493
+
1494
+ financial_results = {
1495
+ "receivable": {"success": False, "data": [], "count": 0, "error": None},
1496
+ "payable": {"success": False, "data": [], "count": 0, "error": None}
1497
+ }
1498
+
1499
+ # Buscar contas a receber
1500
+ if search_type in ["receivable", "both"]:
1501
+ try:
1502
+ receivable_result = await get_sienge_accounts_receivable(
1503
+ start_date=period_start,
1504
+ end_date=period_end,
1505
+ selection_type="D" # Por vencimento
1506
+ )
1507
+
1508
+ if receivable_result["success"]:
1509
+ receivable_data = receivable_result["income_data"]
1510
+
1511
+ # Aplicar filtros de valor se especificados
1512
+ if amount_min is not None or amount_max is not None:
1513
+ filtered_data = []
1514
+ for item in receivable_data:
1515
+ amount = float(item.get("amount", 0) or 0)
1516
+ if amount_min is not None and amount < amount_min:
1517
+ continue
1518
+ if amount_max is not None and amount > amount_max:
1519
+ continue
1520
+ filtered_data.append(item)
1521
+ receivable_data = filtered_data
1522
+
1523
+ # Aplicar filtro de cliente se especificado
1524
+ if customer_creditor_search:
1525
+ search_lower = customer_creditor_search.lower()
1526
+ filtered_data = []
1527
+ for item in receivable_data:
1528
+ customer_name = str(item.get("customer_name", "")).lower()
1529
+ if search_lower in customer_name:
1530
+ filtered_data.append(item)
1531
+ receivable_data = filtered_data
1532
+
1533
+ financial_results["receivable"] = {
1534
+ "success": True,
1535
+ "data": receivable_data,
1536
+ "count": len(receivable_data),
1537
+ "error": None
1538
+ }
1539
+ else:
1540
+ financial_results["receivable"]["error"] = receivable_result.get("error")
1541
+
1542
+ except Exception as e:
1543
+ financial_results["receivable"]["error"] = str(e)
1544
+
1545
+ # Buscar contas a pagar
1546
+ if search_type in ["payable", "both"]:
1547
+ try:
1548
+ payable_result = await get_sienge_bills(
1549
+ start_date=period_start,
1550
+ end_date=period_end,
1551
+ limit=100
1552
+ )
1553
+
1554
+ if payable_result["success"]:
1555
+ payable_data = payable_result["bills"]
1556
+
1557
+ # Aplicar filtros de valor se especificados
1558
+ if amount_min is not None or amount_max is not None:
1559
+ filtered_data = []
1560
+ for item in payable_data:
1561
+ amount = float(item.get("amount", 0) or 0)
1562
+ if amount_min is not None and amount < amount_min:
1563
+ continue
1564
+ if amount_max is not None and amount > amount_max:
1565
+ continue
1566
+ filtered_data.append(item)
1567
+ payable_data = filtered_data
1568
+
1569
+ # Aplicar filtro de credor se especificado
1570
+ if customer_creditor_search:
1571
+ search_lower = customer_creditor_search.lower()
1572
+ filtered_data = []
1573
+ for item in payable_data:
1574
+ creditor_name = str(item.get("creditor_name", "")).lower()
1575
+ if search_lower in creditor_name:
1576
+ filtered_data.append(item)
1577
+ payable_data = filtered_data
1578
+
1579
+ financial_results["payable"] = {
1580
+ "success": True,
1581
+ "data": payable_data,
1582
+ "count": len(payable_data),
1583
+ "error": None
1584
+ }
1585
+ else:
1586
+ financial_results["payable"]["error"] = payable_result.get("error")
1587
+
1588
+ except Exception as e:
1589
+ financial_results["payable"]["error"] = str(e)
1590
+
1591
+ # Compilar resultado final
1592
+ total_records = financial_results["receivable"]["count"] + financial_results["payable"]["count"]
1593
+ has_errors = bool(financial_results["receivable"]["error"] or financial_results["payable"]["error"])
1594
+
1595
+ summary = {
1596
+ "period": f"{period_start} a {period_end}",
1597
+ "search_type": search_type,
1598
+ "total_records": total_records,
1599
+ "receivable_count": financial_results["receivable"]["count"],
1600
+ "payable_count": financial_results["payable"]["count"],
1601
+ "filters_applied": {
1602
+ "amount_range": f"{amount_min or 'sem mín'} - {amount_max or 'sem máx'}",
1603
+ "customer_creditor": customer_creditor_search or "todos"
1604
+ }
1605
+ }
1606
+
1607
+ if total_records > 0:
1608
+ return {
1609
+ "success": True,
1610
+ "message": f"✅ Busca financeira encontrou {total_records} registros no período",
1611
+ "summary": summary,
1612
+ "receivable": financial_results["receivable"],
1613
+ "payable": financial_results["payable"],
1614
+ "has_errors": has_errors
1615
+ }
1616
+ else:
1617
+ return {
1618
+ "success": False,
1619
+ "message": f"❌ Nenhum registro financeiro encontrado no período {period_start} a {period_end}",
1620
+ "summary": summary,
1621
+ "errors": {
1622
+ "receivable": financial_results["receivable"]["error"],
1623
+ "payable": financial_results["payable"]["error"]
1624
+ }
1625
+ }
1626
+
1627
+
1628
+ @mcp.tool
1629
+ async def get_sienge_dashboard_summary() -> Dict:
1630
+ """
1631
+ Obtém um resumo tipo dashboard com informações gerais do Sienge
1632
+ Útil para visão geral rápida do sistema
1633
+ """
1634
+
1635
+ # Data atual e períodos
1636
+ today = datetime.now()
1637
+ current_month_start = today.replace(day=1).strftime("%Y-%m-%d")
1638
+ current_month_end = today.strftime("%Y-%m-%d")
1639
+
1640
+ dashboard_data = {}
1641
+ errors = []
1642
+
1643
+ # 1. Testar conexão
1644
+ try:
1645
+ connection_test = await test_sienge_connection()
1646
+ dashboard_data["connection"] = connection_test
1647
+ except Exception as e:
1648
+ errors.append(f"Teste de conexão: {str(e)}")
1649
+ dashboard_data["connection"] = {"success": False, "error": str(e)}
1650
+
1651
+ # 2. Contar clientes (amostra)
1652
+ try:
1653
+ customers_result = await get_sienge_customers(limit=1)
1654
+ if customers_result["success"]:
1655
+ dashboard_data["customers_available"] = True
1656
+ else:
1657
+ dashboard_data["customers_available"] = False
1658
+ except Exception as e:
1659
+ errors.append(f"Clientes: {str(e)}")
1660
+ dashboard_data["customers_available"] = False
1661
+
1662
+ # 3. Contar projetos (amostra)
1663
+ try:
1664
+ projects_result = await get_sienge_projects(limit=5)
1665
+ if projects_result["success"]:
1666
+ dashboard_data["projects"] = {
1667
+ "available": True,
1668
+ "sample_count": len(projects_result["enterprises"]),
1669
+ "total_count": projects_result.get("metadata", {}).get("count", "N/A")
1670
+ }
1671
+ else:
1672
+ dashboard_data["projects"] = {"available": False}
1673
+ except Exception as e:
1674
+ errors.append(f"Projetos: {str(e)}")
1675
+ dashboard_data["projects"] = {"available": False, "error": str(e)}
1676
+
1677
+ # 4. Títulos a pagar do mês atual
1678
+ try:
1679
+ bills_result = await get_sienge_bills(
1680
+ start_date=current_month_start,
1681
+ end_date=current_month_end,
1682
+ limit=10
1683
+ )
1684
+ if bills_result["success"]:
1685
+ dashboard_data["monthly_bills"] = {
1686
+ "available": True,
1687
+ "count": len(bills_result["bills"]),
1688
+ "total_count": bills_result.get("total_count", len(bills_result["bills"]))
1689
+ }
1690
+ else:
1691
+ dashboard_data["monthly_bills"] = {"available": False}
1692
+ except Exception as e:
1693
+ errors.append(f"Títulos mensais: {str(e)}")
1694
+ dashboard_data["monthly_bills"] = {"available": False, "error": str(e)}
1695
+
1696
+ # 5. Tipos de clientes
1697
+ try:
1698
+ customer_types_result = await get_sienge_customer_types()
1699
+ if customer_types_result["success"]:
1700
+ dashboard_data["customer_types"] = {
1701
+ "available": True,
1702
+ "count": len(customer_types_result["customer_types"])
1703
+ }
1704
+ else:
1705
+ dashboard_data["customer_types"] = {"available": False}
1706
+ except Exception as e:
1707
+ dashboard_data["customer_types"] = {"available": False, "error": str(e)}
1708
+
1709
+ # Compilar resultado
1710
+ available_modules = sum(1 for key, value in dashboard_data.items()
1711
+ if key != "connection" and isinstance(value, dict) and value.get("available"))
1712
+
1713
+ return {
1714
+ "success": True,
1715
+ "message": f"✅ Dashboard do Sienge - {available_modules} módulos disponíveis",
1716
+ "timestamp": today.isoformat(),
1717
+ "period_analyzed": f"{current_month_start} a {current_month_end}",
1718
+ "modules_status": dashboard_data,
1719
+ "available_modules": available_modules,
1720
+ "errors": errors if errors else None,
1721
+ "quick_actions": [
1722
+ "search_sienge_data('termo_busca') - Busca universal",
1723
+ "list_sienge_entities() - Listar tipos de dados",
1724
+ "get_sienge_customers(search='nome') - Buscar clientes",
1725
+ "get_sienge_projects() - Listar projetos/obras",
1726
+ "search_sienge_financial_data('2024-01-01', '2024-12-31') - Dados financeiros"
1727
+ ]
1728
+ }
1729
+
1730
+
1119
1731
  # ============ UTILITÁRIOS ============
1120
1732
 
1733
+
1121
1734
  @mcp.tool
1122
1735
  def add(a: int, b: int) -> int:
1123
1736
  """Soma dois números (função de teste)"""
1124
1737
  return a + b
1125
1738
 
1739
+
1126
1740
  def _get_auth_info_internal() -> Dict:
1127
1741
  """Função interna para verificar configuração de autenticação"""
1128
1742
  if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
1129
- return {
1130
- "auth_method": "Bearer Token",
1131
- "configured": True,
1132
- "base_url": SIENGE_BASE_URL,
1133
- "api_key_configured": True
1134
- }
1743
+ return {"auth_method": "Bearer Token", "configured": True, "base_url": SIENGE_BASE_URL, "api_key_configured": True}
1135
1744
  elif SIENGE_USERNAME and SIENGE_PASSWORD:
1136
1745
  return {
1137
1746
  "auth_method": "Basic Auth",
1138
1747
  "configured": True,
1139
1748
  "base_url": SIENGE_BASE_URL,
1140
1749
  "subdomain": SIENGE_SUBDOMAIN,
1141
- "username": SIENGE_USERNAME
1750
+ "username": SIENGE_USERNAME,
1142
1751
  }
1143
1752
  else:
1144
1753
  return {
1145
1754
  "auth_method": "None",
1146
1755
  "configured": False,
1147
- "message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env"
1756
+ "message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env",
1148
1757
  }
1149
1758
 
1759
+
1150
1760
  @mcp.tool
1151
1761
  def get_auth_info() -> Dict:
1152
1762
  """Retorna informações sobre a configuração de autenticação"""
1153
1763
  return _get_auth_info_internal()
1154
1764
 
1765
+
1155
1766
  def main():
1156
1767
  """Entry point for the Sienge MCP Server"""
1157
1768
  print("* Iniciando Sienge MCP Server (FastMCP)...")
1158
-
1769
+
1159
1770
  # Mostrar info de configuração
1160
1771
  auth_info = _get_auth_info_internal()
1161
1772
  print(f"* Autenticacao: {auth_info['auth_method']}")
1162
1773
  print(f"* Configurado: {auth_info['configured']}")
1163
-
1164
- if not auth_info['configured']:
1774
+
1775
+ if not auth_info["configured"]:
1165
1776
  print("* ERRO: Autenticacao nao configurada!")
1166
1777
  print("Configure as variáveis de ambiente:")
1167
1778
  print("- SIENGE_API_KEY (Bearer Token) OU")
@@ -1179,8 +1790,9 @@ def main():
1179
1790
  print('export SIENGE_SUBDOMAIN="sua_empresa"')
1180
1791
  else:
1181
1792
  print("* MCP pronto para uso!")
1182
-
1793
+
1183
1794
  mcp.run()
1184
1795
 
1796
+
1185
1797
  if __name__ == "__main__":
1186
- main()
1798
+ main()