sienge-ecbiesek-mcp 1.0.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.

@@ -0,0 +1,205 @@
1
+ Metadata-Version: 2.4
2
+ Name: sienge-ecbiesek-mcp
3
+ Version: 1.0.0
4
+ Summary: Sienge ECBIESEK MCP Server - Model Context Protocol integration for Sienge API (ECBIESEK Company)
5
+ Author-email: ECBIESEK <ti@ecbiesek.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Moizas951/Sienge-mcp
8
+ Project-URL: Documentation, https://github.com/Moizas951/Sienge-mcp#readme
9
+ Project-URL: Repository, https://github.com/Moizas951/Sienge-mcp.git
10
+ Project-URL: Issues, https://github.com/Moizas951/Sienge-mcp/issues
11
+ Keywords: sienge,mcp,claude,api,construction,ecbiesek
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Topic :: Office/Business :: Financial
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: fastmcp>=0.1.0
27
+ Requires-Dist: httpx>=0.25.0
28
+ Requires-Dist: pydantic>=2.0.0
29
+ Requires-Dist: python-dotenv>=1.0.0
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
32
+ Requires-Dist: black>=23.0.0; extra == "dev"
33
+ Requires-Dist: flake8>=6.0.0; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # 🏗️ Sienge ECBIESEK MCP Server
37
+
38
+ [![PyPI version](https://badge.fury.io/py/sienge-ecbiesek-mcp.svg)](https://badge.fury.io/py/sienge-ecbiesek-mcp)
39
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
40
+
41
+ **Sienge ECBIESEK MCP Server** - Servidor MCP personalizado para a empresa ECBIESEK integrar com a API do Sienge através do Claude Desktop.
42
+
43
+ ## 🚀 Instalação Rápida (ECBIESEK)
44
+
45
+ ### Usando pipx (Recomendado)
46
+
47
+ ```bash
48
+ # Executar diretamente sem instalação
49
+ pipx run sienge-ecbiesek-mcp
50
+
51
+ # Ou instalar permanentemente
52
+ pipx install sienge-ecbiesek-mcp
53
+ ```
54
+
55
+ ### Usando pip
56
+
57
+ ```bash
58
+ pip install sienge-ecbiesek-mcp
59
+ ```
60
+
61
+ ## ⚙️ Configuração no Claude Desktop (ECBIESEK)
62
+
63
+ Adicione a seguinte configuração ao seu arquivo `claude_desktop_config.json`:
64
+
65
+ ### Para pipx (Recomendado):
66
+ ```json
67
+ {
68
+ "mcpServers": {
69
+ "SiengeECBIESEK": {
70
+ "command": "pipx",
71
+ "args": [
72
+ "run",
73
+ "sienge-ecbiesek-mcp@latest"
74
+ ],
75
+ "env": {
76
+ "SIENGE_BASE_URL": "https://api.sienge.com.br",
77
+ "SIENGE_SUBDOMAIN": "ecbiesek",
78
+ "SIENGE_USERNAME": "seu-usuario-ecbiesek",
79
+ "SIENGE_PASSWORD": "sua-senha-ecbiesek"
80
+ }
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ ### Para pip (instalação global):
87
+ ```json
88
+ {
89
+ "mcpServers": {
90
+ "SiengeECBIESEK": {
91
+ "command": "sienge-ecbiesek-mcp",
92
+ "env": {
93
+ "SIENGE_BASE_URL": "https://api.sienge.com.br",
94
+ "SIENGE_SUBDOMAIN": "ecbiesek",
95
+ "SIENGE_USERNAME": "seu-usuario-ecbiesek",
96
+ "SIENGE_PASSWORD": "sua-senha-ecbiesek"
97
+ }
98
+ }
99
+ }
100
+ }
101
+ ```
102
+
103
+ ## 🔐 Variáveis de Ambiente (ECBIESEK)
104
+
105
+ ### Autenticação Basic Auth (Configuração ECBIESEK)
106
+ ```bash
107
+ SIENGE_USERNAME=usuario_ecbiesek
108
+ SIENGE_PASSWORD=senha_ecbiesek
109
+ SIENGE_SUBDOMAIN=ecbiesek
110
+ SIENGE_BASE_URL=https://api.sienge.com.br
111
+ ```
112
+
113
+ ### Autenticação Bearer Token (Alternativa)
114
+ ```bash
115
+ SIENGE_API_KEY=token_api_ecbiesek
116
+ SIENGE_BASE_URL=https://api.sienge.com.br
117
+ ```
118
+
119
+ ## 🛠️ Ferramentas Disponíveis
120
+
121
+ O servidor ECBIESEK fornece mais de 30 ferramentas para interagir com a API do Sienge:
122
+
123
+ ### 📊 Relatórios e Listagens
124
+ - `listar_projetos` - Lista todos os projetos ECBIESEK
125
+ - `listar_contas` - Lista contas a pagar/receber
126
+ - `listar_centros_custo` - Lista centros de custo
127
+ - `listar_fornecedores` - Lista fornecedores
128
+ - `listar_colaboradores` - Lista colaboradores ECBIESEK
129
+
130
+ ### 🏗️ Gestão de Projetos
131
+ - `obter_projeto` - Obtém detalhes de um projeto específico
132
+ - `listar_etapas_projeto` - Lista etapas de um projeto
133
+ - `listar_medicoes_projeto` - Lista medições de um projeto
134
+
135
+ ### 💰 Financeiro
136
+ - `criar_conta_pagar` - Cria conta a pagar
137
+ - `atualizar_conta` - Atualiza conta existente
138
+ - `obter_detalhes_conta` - Obtém detalhes de uma conta
139
+
140
+ ### 👥 Recursos Humanos ECBIESEK
141
+ - `obter_colaborador` - Obtém detalhes de um colaborador
142
+ - `listar_departamentos` - Lista departamentos
143
+ - `listar_cargos` - Lista cargos
144
+
145
+ ### 📦 Estoque e Suprimentos
146
+ - `listar_produtos` - Lista produtos
147
+ - `obter_produto` - Obtém detalhes de um produto
148
+ - `listar_grupos_produto` - Lista grupos de produtos
149
+
150
+ ### ⚙️ Configurações
151
+ - `obter_configuracoes_sistema` - Obtém configurações do sistema
152
+ - `listar_moedas` - Lista moedas disponíveis
153
+ - `obter_info_autenticacao` - Verifica status da autenticação
154
+
155
+ ## 📝 Exemplos de Uso (ECBIESEK)
156
+
157
+ ### Listar Projetos
158
+ ```
159
+ Claude: Liste todos os projetos ativos da ECBIESEK no Sienge.
160
+ ```
161
+
162
+ ### Criar Conta a Pagar
163
+ ```
164
+ Claude: Crie uma conta a pagar para o fornecedor XYZ no valor de R$ 1.500,00 com vencimento em 30 dias.
165
+ ```
166
+
167
+ ### Obter Relatório Financeiro
168
+ ```
169
+ Claude: Me mostre um resumo das contas a pagar em aberto dos projetos da ECBIESEK.
170
+ ```
171
+
172
+ ## 🏢 Sobre a ECBIESEK
173
+
174
+ Este MCP Server foi desenvolvido especificamente para atender às necessidades da empresa ECBIESEK na integração com o sistema Sienge.
175
+
176
+ ## 📋 Requisitos
177
+
178
+ - Python 3.9 ou superior
179
+ - Acesso à API do Sienge com credenciais ECBIESEK
180
+ - Claude Desktop instalado
181
+
182
+ ## 🐛 Resolução de Problemas
183
+
184
+ ### Erro de Autenticação
185
+ - Verifique se as credenciais ECBIESEK estão corretas
186
+ - Confirme se o subdomínio "ecbiesek" está correto
187
+ - Teste as credenciais diretamente na API do Sienge
188
+
189
+ ### Timeout de Requisições
190
+ - Aumente o valor de `REQUEST_TIMEOUT`
191
+ - Verifique a conectividade com a API do Sienge
192
+
193
+ ## 📄 Licença
194
+
195
+ MIT License - Desenvolvido para ECBIESEK
196
+
197
+ ## 📞 Suporte ECBIESEK
198
+
199
+ - 📧 Email: ti@ecbiesek.com
200
+ - 🏢 Empresa: ECBIESEK
201
+ - 📖 Documentação interna
202
+
203
+ ---
204
+
205
+ **⭐ Sienge ECBIESEK MCP Server - Solução personalizada para integração Sienge + Claude Desktop**
@@ -0,0 +1,9 @@
1
+ sienge_ecbiesek_mcp-1.0.0.dist-info/licenses/LICENSE,sha256=leWD46QLXsQ43M8fE_KgOo5Sf0YB9_X8EVqGdV0Dsc0,1101
2
+ sienge_mcp/__init__.py,sha256=Pjl4hgBCWhVJ_BBZXaP7SuZfH7Z1JWZbSs8MV5sUle8,287
3
+ sienge_mcp/server.py,sha256=-7JO_QGvI2Bsw2Fg3kcAmVoLLVoEsffZiHwjw3jLPzc,27107
4
+ sienge_mcp/utils/logger.py,sha256=bqU0GDsQXE9TaKOq5_6S2L8bh_Nas-EuYNDE3fzlPWg,5880
5
+ sienge_ecbiesek_mcp-1.0.0.dist-info/METADATA,sha256=kUMzLjwbt_ACAuUJI5M-n6PIuewJP6cwd4ZqX_WfFyI,6318
6
+ sienge_ecbiesek_mcp-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ sienge_ecbiesek_mcp-1.0.0.dist-info/entry_points.txt,sha256=jxEu6gvTw3ci0mjDfqbi0rBLRpeuscwwRk9-H-UOnO8,63
8
+ sienge_ecbiesek_mcp-1.0.0.dist-info/top_level.txt,sha256=FCvuhB9JQPKGY0Q8aKoVc7akqG5htoJyfj-eJvVUmWM,11
9
+ sienge_ecbiesek_mcp-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ sienge-ecbiesek-mcp = sienge_mcp.server:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Sienge MCP Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ sienge_mcp
sienge_mcp/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ """
2
+ Sienge MCP Server Package
3
+ Model Context Protocol integration for Sienge API
4
+ """
5
+
6
+ __version__ = "1.0.0"
7
+ __author__ = "Sienge MCP Contributors"
8
+ __email__ = "contributors@sienge-mcp.com"
9
+ __description__ = "Sienge MCP Server - Model Context Protocol integration for Sienge API"
sienge_mcp/server.py ADDED
@@ -0,0 +1,783 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ SIENGE MCP COMPLETO - FastMCP com Autenticação Flexível
4
+ Suporta Bearer Token e Basic Auth
5
+ """
6
+
7
+ from fastmcp import FastMCP
8
+ import httpx
9
+ import asyncio
10
+ from typing import Dict, List, Optional, Any
11
+ import json
12
+ import os
13
+ from dotenv import load_dotenv
14
+ from datetime import datetime
15
+ import base64
16
+
17
+ # Carrega as variáveis de ambiente
18
+ load_dotenv()
19
+
20
+ mcp = FastMCP("Sienge API Integration 🏗️")
21
+
22
+ # Configurações da API do Sienge
23
+ SIENGE_BASE_URL = os.getenv("SIENGE_BASE_URL", "https://api.sienge.com.br")
24
+ SIENGE_SUBDOMAIN = os.getenv("SIENGE_SUBDOMAIN", "")
25
+ SIENGE_USERNAME = os.getenv("SIENGE_USERNAME", "")
26
+ SIENGE_PASSWORD = os.getenv("SIENGE_PASSWORD", "")
27
+ SIENGE_API_KEY = os.getenv("SIENGE_API_KEY", "")
28
+ REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", "30"))
29
+
30
+ class SiengeAPIError(Exception):
31
+ """Exceção customizada para erros da API do Sienge"""
32
+ pass
33
+
34
+ async def make_sienge_request(method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None) -> Dict:
35
+ """
36
+ Função auxiliar para fazer requisições à API do Sienge
37
+ Suporta tanto Bearer Token quanto Basic Auth
38
+ """
39
+ try:
40
+ async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
41
+ headers = {
42
+ "Content-Type": "application/json",
43
+ "Accept": "application/json"
44
+ }
45
+
46
+ # Configurar autenticação e URL
47
+ auth = None
48
+
49
+ if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
50
+ # Bearer Token (Recomendado)
51
+ headers["Authorization"] = f"Bearer {SIENGE_API_KEY}"
52
+ url = f"{SIENGE_BASE_URL}/{SIENGE_SUBDOMAIN}/public/api/v1{endpoint}"
53
+ elif SIENGE_USERNAME and SIENGE_PASSWORD:
54
+ # Basic Auth usando httpx.BasicAuth
55
+ auth = httpx.BasicAuth(SIENGE_USERNAME, SIENGE_PASSWORD)
56
+ url = f"{SIENGE_BASE_URL}/{SIENGE_SUBDOMAIN}/public/api/v1{endpoint}"
57
+ else:
58
+ return {
59
+ "success": False,
60
+ "error": "No Authentication",
61
+ "message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env"
62
+ }
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
+
73
+ if response.status_code in [200, 201]:
74
+ 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
+ }
86
+ else:
87
+ return {
88
+ "success": False,
89
+ "error": f"HTTP {response.status_code}",
90
+ "message": response.text,
91
+ "status_code": response.status_code
92
+ }
93
+
94
+ 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
+ }
100
+ except Exception as e:
101
+ return {
102
+ "success": False,
103
+ "error": str(e),
104
+ "message": f"Erro na requisição: {str(e)}"
105
+ }
106
+
107
+ # ============ CONEXÃO E TESTE ============
108
+
109
+ @mcp.tool
110
+ async def test_sienge_connection() -> Dict:
111
+ """Testa a conexão com a API do Sienge"""
112
+ try:
113
+ # Tentar endpoint mais simples primeiro
114
+ result = await make_sienge_request("GET", "/customer-types")
115
+
116
+ if result["success"]:
117
+ auth_method = "Bearer Token" if SIENGE_API_KEY else "Basic Auth"
118
+ return {
119
+ "success": True,
120
+ "message": "✅ Conexão com API do Sienge estabelecida com sucesso!",
121
+ "api_status": "Online",
122
+ "auth_method": auth_method,
123
+ "timestamp": datetime.now().isoformat()
124
+ }
125
+ else:
126
+ return {
127
+ "success": False,
128
+ "message": "❌ Falha ao conectar com API do Sienge",
129
+ "error": result.get("error"),
130
+ "details": result.get("message"),
131
+ "timestamp": datetime.now().isoformat()
132
+ }
133
+ except Exception as e:
134
+ return {
135
+ "success": False,
136
+ "message": "❌ Erro ao testar conexão",
137
+ "error": str(e),
138
+ "timestamp": datetime.now().isoformat()
139
+ }
140
+
141
+ # ============ CLIENTES ============
142
+
143
+ @mcp.tool
144
+ async def get_sienge_customers(limit: Optional[int] = 50, offset: Optional[int] = 0, search: Optional[str] = None, customer_type_id: Optional[str] = None) -> Dict:
145
+ """
146
+ Busca clientes no Sienge com filtros
147
+
148
+ Args:
149
+ limit: Máximo de registros (padrão: 50)
150
+ offset: Pular registros (padrão: 0)
151
+ search: Buscar por nome ou documento
152
+ customer_type_id: Filtrar por tipo de cliente
153
+ """
154
+ params = {"limit": min(limit or 50, 200), "offset": offset or 0}
155
+
156
+ if search:
157
+ params["search"] = search
158
+ if customer_type_id:
159
+ params["customer_type_id"] = customer_type_id
160
+
161
+ result = await make_sienge_request("GET", "/customers", params=params)
162
+
163
+ if result["success"]:
164
+ data = result["data"]
165
+ customers = data.get("results", []) if isinstance(data, dict) else data
166
+ metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
167
+ total_count = metadata.get("count", len(customers))
168
+
169
+ return {
170
+ "success": True,
171
+ "message": f"✅ Encontrados {len(customers)} clientes (total: {total_count})",
172
+ "customers": customers,
173
+ "count": len(customers),
174
+ "filters_applied": params
175
+ }
176
+
177
+ return {
178
+ "success": False,
179
+ "message": "❌ Erro ao buscar clientes",
180
+ "error": result.get("error"),
181
+ "details": result.get("message")
182
+ }
183
+
184
+ @mcp.tool
185
+ async def get_sienge_customer_types() -> Dict:
186
+ """Lista tipos de clientes disponíveis"""
187
+ result = await make_sienge_request("GET", "/customer-types")
188
+
189
+ if result["success"]:
190
+ data = result["data"]
191
+ customer_types = data.get("results", []) if isinstance(data, dict) else data
192
+ metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
193
+ total_count = metadata.get("count", len(customer_types))
194
+
195
+ return {
196
+ "success": True,
197
+ "message": f"✅ Encontrados {len(customer_types)} tipos de clientes (total: {total_count})",
198
+ "customer_types": customer_types,
199
+ "count": len(customer_types)
200
+ }
201
+
202
+ return {
203
+ "success": False,
204
+ "message": "❌ Erro ao buscar tipos de clientes",
205
+ "error": result.get("error"),
206
+ "details": result.get("message")
207
+ }
208
+
209
+ # ============ CREDORES ============
210
+
211
+ @mcp.tool
212
+ async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int] = 0, search: Optional[str] = None) -> Dict:
213
+ """
214
+ Busca credores/fornecedores
215
+
216
+ Args:
217
+ limit: Máximo de registros (padrão: 50)
218
+ offset: Pular registros (padrão: 0)
219
+ search: Buscar por nome
220
+ """
221
+ params = {"limit": min(limit or 50, 200), "offset": offset or 0}
222
+ if search:
223
+ params["search"] = search
224
+
225
+ result = await make_sienge_request("GET", "/creditors", params=params)
226
+
227
+ if result["success"]:
228
+ data = result["data"]
229
+ creditors = data.get("results", []) if isinstance(data, dict) else data
230
+ metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
231
+ total_count = metadata.get("count", len(creditors))
232
+
233
+ return {
234
+ "success": True,
235
+ "message": f"✅ Encontrados {len(creditors)} credores (total: {total_count})",
236
+ "creditors": creditors,
237
+ "count": len(creditors)
238
+ }
239
+
240
+ return {
241
+ "success": False,
242
+ "message": "❌ Erro ao buscar credores",
243
+ "error": result.get("error"),
244
+ "details": result.get("message")
245
+ }
246
+
247
+ @mcp.tool
248
+ async def get_sienge_creditor_bank_info(creditor_id: str) -> Dict:
249
+ """
250
+ Consulta informações bancárias de um credor
251
+
252
+ Args:
253
+ creditor_id: ID do credor (obrigatório)
254
+ """
255
+ result = await make_sienge_request("GET", f"/creditors/{creditor_id}/bank-informations")
256
+
257
+ if result["success"]:
258
+ return {
259
+ "success": True,
260
+ "message": f"✅ Informações bancárias do credor {creditor_id}",
261
+ "creditor_id": creditor_id,
262
+ "bank_info": result["data"]
263
+ }
264
+
265
+ return {
266
+ "success": False,
267
+ "message": f"❌ Erro ao buscar info bancária do credor {creditor_id}",
268
+ "error": result.get("error"),
269
+ "details": result.get("message")
270
+ }
271
+
272
+ # ============ FINANCEIRO ============
273
+
274
+ @mcp.tool
275
+ async def get_sienge_accounts_receivable(customer_id: Optional[str] = None, due_date_from: Optional[str] = None,
276
+ due_date_to: Optional[str] = None, status: Optional[str] = None, limit: Optional[int] = 50) -> Dict:
277
+ """
278
+ Consulta títulos a receber
279
+
280
+ Args:
281
+ customer_id: ID do cliente
282
+ due_date_from: Data inicial (YYYY-MM-DD)
283
+ due_date_to: Data final (YYYY-MM-DD)
284
+ status: Status (open, paid, overdue)
285
+ limit: Máximo de registros
286
+ """
287
+ params = {"limit": min(limit or 50, 200)}
288
+
289
+ if customer_id:
290
+ params["customer_id"] = customer_id
291
+ if due_date_from:
292
+ params["due_date_from"] = due_date_from
293
+ if due_date_to:
294
+ params["due_date_to"] = due_date_to
295
+ if status:
296
+ params["status"] = status
297
+
298
+ result = await make_sienge_request("GET", "/accounts-receivable", params=params)
299
+
300
+ if result["success"]:
301
+ data = result["data"]
302
+ receivables = data.get("results", []) if isinstance(data, dict) else data
303
+
304
+ return {
305
+ "success": True,
306
+ "message": f"✅ Encontrados {len(receivables)} títulos a receber",
307
+ "receivables": receivables,
308
+ "count": len(receivables),
309
+ "filters": params
310
+ }
311
+
312
+ return {
313
+ "success": False,
314
+ "message": "❌ Erro ao buscar títulos a receber",
315
+ "error": result.get("error"),
316
+ "details": result.get("message")
317
+ }
318
+
319
+ @mcp.tool
320
+ async def get_sienge_bills(start_date: Optional[str] = None, end_date: Optional[str] = None,
321
+ creditor_id: Optional[str] = None, status: Optional[str] = None,
322
+ limit: Optional[int] = 50) -> Dict:
323
+ """
324
+ Consulta títulos a pagar (contas a pagar) - REQUER startDate obrigatório
325
+
326
+ Args:
327
+ start_date: Data inicial obrigatória (YYYY-MM-DD) - padrão últimos 30 dias
328
+ end_date: Data final (YYYY-MM-DD) - padrão hoje
329
+ creditor_id: ID do credor
330
+ status: Status do título (ex: open, paid, cancelled)
331
+ limit: Máximo de registros (padrão: 50, máx: 200)
332
+ """
333
+ from datetime import datetime, timedelta
334
+
335
+ # Se start_date não fornecido, usar últimos 30 dias
336
+ if not start_date:
337
+ start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
338
+
339
+ # Se end_date não fornecido, usar hoje
340
+ if not end_date:
341
+ end_date = datetime.now().strftime('%Y-%m-%d')
342
+
343
+ # Parâmetros obrigatórios
344
+ params = {
345
+ "startDate": start_date, # OBRIGATÓRIO pela API
346
+ "endDate": end_date,
347
+ "limit": min(limit or 50, 200)
348
+ }
349
+
350
+ # Parâmetros opcionais
351
+ if creditor_id:
352
+ params["creditor_id"] = creditor_id
353
+ if status:
354
+ params["status"] = status
355
+
356
+ result = await make_sienge_request("GET", "/bills", params=params)
357
+
358
+ if result["success"]:
359
+ data = result["data"]
360
+ bills = data.get("results", []) if isinstance(data, dict) else data
361
+ metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
362
+ total_count = metadata.get("count", len(bills))
363
+
364
+ return {
365
+ "success": True,
366
+ "message": f"✅ Encontrados {len(bills)} títulos a pagar (total: {total_count}) - período: {start_date} a {end_date}",
367
+ "bills": bills,
368
+ "count": len(bills),
369
+ "total_count": total_count,
370
+ "period": {"start_date": start_date, "end_date": end_date},
371
+ "filters": params
372
+ }
373
+
374
+ return {
375
+ "success": False,
376
+ "message": "❌ Erro ao buscar títulos a pagar",
377
+ "error": result.get("error"),
378
+ "details": result.get("message")
379
+ }
380
+
381
+ # ============ COMPRAS ============
382
+
383
+ @mcp.tool
384
+ async def get_sienge_purchase_orders(purchase_order_id: Optional[str] = None, status: Optional[str] = None,
385
+ date_from: Optional[str] = None, limit: Optional[int] = 50) -> Dict:
386
+ """
387
+ Consulta pedidos de compra
388
+
389
+ Args:
390
+ purchase_order_id: ID específico do pedido
391
+ status: Status do pedido
392
+ date_from: Data inicial (YYYY-MM-DD)
393
+ limit: Máximo de registros
394
+ """
395
+ if purchase_order_id:
396
+ result = await make_sienge_request("GET", f"/purchase-orders/{purchase_order_id}")
397
+ if result["success"]:
398
+ return {
399
+ "success": True,
400
+ "message": f"✅ Pedido {purchase_order_id} encontrado",
401
+ "purchase_order": result["data"]
402
+ }
403
+ return result
404
+
405
+ params = {"limit": min(limit or 50, 200)}
406
+ if status:
407
+ params["status"] = status
408
+ if date_from:
409
+ params["date_from"] = date_from
410
+
411
+ result = await make_sienge_request("GET", "/purchase-orders", params=params)
412
+
413
+ if result["success"]:
414
+ data = result["data"]
415
+ orders = data.get("results", []) if isinstance(data, dict) else data
416
+
417
+ return {
418
+ "success": True,
419
+ "message": f"✅ Encontrados {len(orders)} pedidos de compra",
420
+ "purchase_orders": orders,
421
+ "count": len(orders)
422
+ }
423
+
424
+ return {
425
+ "success": False,
426
+ "message": "❌ Erro ao buscar pedidos de compra",
427
+ "error": result.get("error"),
428
+ "details": result.get("message")
429
+ }
430
+
431
+ @mcp.tool
432
+ async def get_sienge_purchase_order_items(purchase_order_id: str) -> Dict:
433
+ """
434
+ Consulta itens de um pedido de compra específico
435
+
436
+ Args:
437
+ purchase_order_id: ID do pedido (obrigatório)
438
+ """
439
+ result = await make_sienge_request("GET", f"/purchase-orders/{purchase_order_id}/items")
440
+
441
+ if result["success"]:
442
+ data = result["data"]
443
+ items = data.get("results", []) if isinstance(data, dict) else data
444
+
445
+ return {
446
+ "success": True,
447
+ "message": f"✅ Encontrados {len(items)} itens no pedido {purchase_order_id}",
448
+ "purchase_order_id": purchase_order_id,
449
+ "items": items,
450
+ "count": len(items)
451
+ }
452
+
453
+ return {
454
+ "success": False,
455
+ "message": f"❌ Erro ao buscar itens do pedido {purchase_order_id}",
456
+ "error": result.get("error"),
457
+ "details": result.get("message")
458
+ }
459
+
460
+ @mcp.tool
461
+ async def get_sienge_purchase_requests(purchase_request_id: Optional[str] = None, limit: Optional[int] = 50) -> Dict:
462
+ """
463
+ Consulta solicitações de compra
464
+
465
+ Args:
466
+ purchase_request_id: ID específico da solicitação
467
+ limit: Máximo de registros
468
+ """
469
+ if purchase_request_id:
470
+ result = await make_sienge_request("GET", f"/purchase-requests/{purchase_request_id}")
471
+ if result["success"]:
472
+ return {
473
+ "success": True,
474
+ "message": f"✅ Solicitação {purchase_request_id} encontrada",
475
+ "purchase_request": result["data"]
476
+ }
477
+ return result
478
+
479
+ params = {"limit": min(limit or 50, 200)}
480
+ result = await make_sienge_request("GET", "/purchase-requests", params=params)
481
+
482
+ if result["success"]:
483
+ data = result["data"]
484
+ requests = data.get("results", []) if isinstance(data, dict) else data
485
+
486
+ return {
487
+ "success": True,
488
+ "message": f"✅ Encontradas {len(requests)} solicitações de compra",
489
+ "purchase_requests": requests,
490
+ "count": len(requests)
491
+ }
492
+
493
+ return {
494
+ "success": False,
495
+ "message": "❌ Erro ao buscar solicitações de compra",
496
+ "error": result.get("error"),
497
+ "details": result.get("message")
498
+ }
499
+
500
+ @mcp.tool
501
+ async def create_sienge_purchase_request(description: str, project_id: str, items: List[Dict[str, Any]]) -> Dict:
502
+ """
503
+ Cria nova solicitação de compra
504
+
505
+ Args:
506
+ description: Descrição da solicitação
507
+ project_id: ID do projeto/obra
508
+ items: Lista de itens da solicitação
509
+ """
510
+ request_data = {
511
+ "description": description,
512
+ "project_id": project_id,
513
+ "items": items,
514
+ "date": datetime.now().strftime("%Y-%m-%d")
515
+ }
516
+
517
+ result = await make_sienge_request("POST", "/purchase-requests", json_data=request_data)
518
+
519
+ if result["success"]:
520
+ return {
521
+ "success": True,
522
+ "message": "✅ Solicitação de compra criada com sucesso",
523
+ "request_id": result["data"].get("id"),
524
+ "data": result["data"]
525
+ }
526
+
527
+ return {
528
+ "success": False,
529
+ "message": "❌ Erro ao criar solicitação de compra",
530
+ "error": result.get("error"),
531
+ "details": result.get("message")
532
+ }
533
+
534
+ # ============ ESTOQUE ============
535
+
536
+ @mcp.tool
537
+ async def get_sienge_stock_inventory(cost_center_id: str, resource_id: Optional[str] = None) -> Dict:
538
+ """
539
+ Consulta inventário de estoque por centro de custo
540
+
541
+ Args:
542
+ cost_center_id: ID do centro de custo (obrigatório)
543
+ resource_id: ID do insumo específico (opcional)
544
+ """
545
+ if resource_id:
546
+ endpoint = f"/stock-inventories/{cost_center_id}/items/{resource_id}"
547
+ else:
548
+ endpoint = f"/stock-inventories/{cost_center_id}/items"
549
+
550
+ result = await make_sienge_request("GET", endpoint)
551
+
552
+ if result["success"]:
553
+ data = result["data"]
554
+ items = data.get("results", []) if isinstance(data, dict) else data
555
+ count = len(items) if isinstance(items, list) else 1
556
+
557
+ return {
558
+ "success": True,
559
+ "message": f"✅ Inventário do centro de custo {cost_center_id}",
560
+ "cost_center_id": cost_center_id,
561
+ "inventory": items,
562
+ "count": count
563
+ }
564
+
565
+ return {
566
+ "success": False,
567
+ "message": f"❌ Erro ao consultar estoque do centro {cost_center_id}",
568
+ "error": result.get("error"),
569
+ "details": result.get("message")
570
+ }
571
+
572
+ @mcp.tool
573
+ async def get_sienge_stock_reservations(limit: Optional[int] = 50) -> Dict:
574
+ """
575
+ Lista reservas de estoque
576
+
577
+ Args:
578
+ limit: Máximo de registros
579
+ """
580
+ params = {"limit": min(limit or 50, 200)}
581
+ result = await make_sienge_request("GET", "/stock-reservations", params=params)
582
+
583
+ if result["success"]:
584
+ data = result["data"]
585
+ reservations = data.get("results", []) if isinstance(data, dict) else data
586
+
587
+ return {
588
+ "success": True,
589
+ "message": f"✅ Encontradas {len(reservations)} reservas de estoque",
590
+ "reservations": reservations,
591
+ "count": len(reservations)
592
+ }
593
+
594
+ return {
595
+ "success": False,
596
+ "message": "❌ Erro ao buscar reservas de estoque",
597
+ "error": result.get("error"),
598
+ "details": result.get("message")
599
+ }
600
+
601
+ # ============ PROJETOS/OBRAS ============
602
+
603
+ @mcp.tool
604
+ async def get_sienge_projects(limit: Optional[int] = 50, offset: Optional[int] = 0, status: Optional[str] = None, search: Optional[str] = None) -> Dict:
605
+ """
606
+ Busca projetos/obras no Sienge
607
+
608
+ Args:
609
+ limit: Máximo de registros (padrão: 50)
610
+ offset: Pular registros (padrão: 0)
611
+ status: Filtrar por status
612
+ search: Buscar por nome
613
+ """
614
+ params = {"limit": min(limit or 50, 200), "offset": offset or 0}
615
+
616
+ if status:
617
+ params["status"] = status
618
+ if search:
619
+ params["search"] = search
620
+
621
+ result = await make_sienge_request("GET", "/projects", params=params)
622
+
623
+ if result["success"]:
624
+ data = result["data"]
625
+ projects = data.get("results", []) if isinstance(data, dict) else data
626
+
627
+ return {
628
+ "success": True,
629
+ "message": f"✅ Encontrados {len(projects)} projetos",
630
+ "projects": projects,
631
+ "count": len(projects)
632
+ }
633
+
634
+ return {
635
+ "success": False,
636
+ "message": "❌ Erro ao buscar projetos",
637
+ "error": result.get("error"),
638
+ "details": result.get("message")
639
+ }
640
+
641
+ @mcp.tool
642
+ async def get_sienge_units(limit: Optional[int] = 50, offset: Optional[int] = 0) -> Dict:
643
+ """
644
+ Consulta unidades cadastradas no Sienge
645
+
646
+ Args:
647
+ limit: Máximo de registros (padrão: 50)
648
+ offset: Pular registros (padrão: 0)
649
+ """
650
+ params = {"limit": min(limit or 50, 200), "offset": offset or 0}
651
+ result = await make_sienge_request("GET", "/units", params=params)
652
+
653
+ if result["success"]:
654
+ data = result["data"]
655
+ units = data.get("results", []) if isinstance(data, dict) else data
656
+ metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
657
+ total_count = metadata.get("count", len(units))
658
+
659
+ return {
660
+ "success": True,
661
+ "message": f"✅ Encontradas {len(units)} unidades (total: {total_count})",
662
+ "units": units,
663
+ "count": len(units)
664
+ }
665
+
666
+ return {
667
+ "success": False,
668
+ "message": "❌ Erro ao buscar unidades",
669
+ "error": result.get("error"),
670
+ "details": result.get("message")
671
+ }
672
+
673
+ # ============ CUSTOS ============
674
+
675
+ @mcp.tool
676
+ async def get_sienge_unit_cost_tables(table_code: Optional[str] = None, description: Optional[str] = None,
677
+ status: Optional[str] = "Active", integration_enabled: Optional[bool] = None) -> Dict:
678
+ """
679
+ Consulta tabelas de custos unitários
680
+
681
+ Args:
682
+ table_code: Código da tabela (opcional)
683
+ description: Descrição da tabela (opcional)
684
+ status: Status (Active/Inactive)
685
+ integration_enabled: Se habilitada para integração
686
+ """
687
+ params = {"status": status or "Active"}
688
+
689
+ if table_code:
690
+ params["table_code"] = table_code
691
+ if description:
692
+ params["description"] = description
693
+ if integration_enabled is not None:
694
+ params["integration_enabled"] = integration_enabled
695
+
696
+ result = await make_sienge_request("GET", "/unit-cost-tables", params=params)
697
+
698
+ if result["success"]:
699
+ data = result["data"]
700
+ tables = data.get("results", []) if isinstance(data, dict) else data
701
+
702
+ return {
703
+ "success": True,
704
+ "message": f"✅ Encontradas {len(tables)} tabelas de custos",
705
+ "cost_tables": tables,
706
+ "count": len(tables)
707
+ }
708
+
709
+ return {
710
+ "success": False,
711
+ "message": "❌ Erro ao buscar tabelas de custos",
712
+ "error": result.get("error"),
713
+ "details": result.get("message")
714
+ }
715
+
716
+ # ============ UTILITÁRIOS ============
717
+
718
+ @mcp.tool
719
+ def add(a: int, b: int) -> int:
720
+ """Soma dois números (função de teste)"""
721
+ return a + b
722
+
723
+ def _get_auth_info_internal() -> Dict:
724
+ """Função interna para verificar configuração de autenticação"""
725
+ if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
726
+ return {
727
+ "auth_method": "Bearer Token",
728
+ "configured": True,
729
+ "base_url": SIENGE_BASE_URL,
730
+ "api_key_configured": True
731
+ }
732
+ elif SIENGE_USERNAME and SIENGE_PASSWORD:
733
+ return {
734
+ "auth_method": "Basic Auth",
735
+ "configured": True,
736
+ "base_url": SIENGE_BASE_URL,
737
+ "subdomain": SIENGE_SUBDOMAIN,
738
+ "username": SIENGE_USERNAME
739
+ }
740
+ else:
741
+ return {
742
+ "auth_method": "None",
743
+ "configured": False,
744
+ "message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env"
745
+ }
746
+
747
+ @mcp.tool
748
+ def get_auth_info() -> Dict:
749
+ """Retorna informações sobre a configuração de autenticação"""
750
+ return _get_auth_info_internal()
751
+
752
+ def main():
753
+ """Entry point for the Sienge MCP Server"""
754
+ print("* Iniciando Sienge MCP Server (FastMCP)...")
755
+
756
+ # Mostrar info de configuração
757
+ auth_info = _get_auth_info_internal()
758
+ print(f"* Autenticacao: {auth_info['auth_method']}")
759
+ print(f"* Configurado: {auth_info['configured']}")
760
+
761
+ if not auth_info['configured']:
762
+ print("* ERRO: Autenticacao nao configurada!")
763
+ print("Configure as variáveis de ambiente:")
764
+ print("- SIENGE_API_KEY (Bearer Token) OU")
765
+ print("- SIENGE_USERNAME + SIENGE_PASSWORD + SIENGE_SUBDOMAIN (Basic Auth)")
766
+ print("- SIENGE_BASE_URL (padrão: https://api.sienge.com.br)")
767
+ print("")
768
+ print("Exemplo no Windows PowerShell:")
769
+ print('$env:SIENGE_USERNAME="seu_usuario"')
770
+ print('$env:SIENGE_PASSWORD="sua_senha"')
771
+ print('$env:SIENGE_SUBDOMAIN="sua_empresa"')
772
+ print("")
773
+ print("Exemplo no Linux/Mac:")
774
+ print('export SIENGE_USERNAME="seu_usuario"')
775
+ print('export SIENGE_PASSWORD="sua_senha"')
776
+ print('export SIENGE_SUBDOMAIN="sua_empresa"')
777
+ else:
778
+ print("* MCP pronto para uso!")
779
+
780
+ mcp.run()
781
+
782
+ if __name__ == "__main__":
783
+ main()
@@ -0,0 +1,169 @@
1
+ """
2
+ SPDX-FileCopyrightText: © 2025 Moizes Silva
3
+ SPDX-License-Identifier: MIT
4
+
5
+ Logger module for Sienge MCP Server
6
+
7
+ This module provides logging functionality for the server,
8
+ writing logs to a file to avoid interfering with MCP communication.
9
+ """
10
+
11
+ import logging
12
+ import os
13
+ import sys
14
+ from datetime import datetime
15
+ from enum import Enum
16
+ from pathlib import Path
17
+ from typing import Any, Optional, Dict
18
+
19
+
20
+ class LogLevel(Enum):
21
+ """Log levels enum"""
22
+ TRACE = 5
23
+ DEBUG = 10
24
+ INFO = 20
25
+ WARN = 30
26
+ ERROR = 40
27
+
28
+
29
+ class SiengeLogger:
30
+ """
31
+ Professional logging system for Sienge MCP Server
32
+ Based on ClickUp MCP logging patterns
33
+ """
34
+
35
+ def __init__(self, name: str = "SiengeMCP"):
36
+ self.name = name
37
+ self.pid = os.getpid()
38
+ self.logger = self._setup_logger()
39
+
40
+ def _setup_logger(self) -> logging.Logger:
41
+ """Setup logger with file output only"""
42
+ logger = logging.getLogger(self.name)
43
+ logger.setLevel(logging.DEBUG)
44
+
45
+ # Prevent duplicate handlers
46
+ if logger.handlers:
47
+ return logger
48
+
49
+ # Create logs directory if it doesn't exist
50
+ log_dir = Path(__file__).parent.parent.parent.parent / "logs"
51
+ log_dir.mkdir(exist_ok=True)
52
+
53
+ # Create file handler
54
+ log_file = log_dir / "sienge-mcp.log"
55
+ file_handler = logging.FileHandler(log_file, mode='w', encoding='utf-8')
56
+
57
+ # Create formatter
58
+ formatter = logging.Formatter(
59
+ '[%(asctime)s] [PID:%(process)d] %(levelname)s [%(name)s]: %(message)s',
60
+ datefmt='%Y-%m-%dT%H:%M:%S.%fZ'
61
+ )
62
+ file_handler.setFormatter(formatter)
63
+
64
+ logger.addHandler(file_handler)
65
+
66
+ # Log initialization
67
+ logger.info(f"Logger initialized for {self.name}")
68
+
69
+ return logger
70
+
71
+ def _format_data(self, data: Any) -> str:
72
+ """Format data for logging"""
73
+ if data is None:
74
+ return ""
75
+
76
+ if isinstance(data, dict):
77
+ if len(data) <= 4 and all(not isinstance(v, (dict, list)) or v is None for v in data.values()):
78
+ # Simple object with few properties - format inline
79
+ items = [f"{k}={v}" for k, v in data.items()]
80
+ return f" | {' | '.join(items)}"
81
+ else:
82
+ # Complex object - format as JSON
83
+ import json
84
+ try:
85
+ return f" | {json.dumps(data, indent=2, ensure_ascii=False)}"
86
+ except (TypeError, ValueError):
87
+ return f" | {str(data)}"
88
+
89
+ return f" | {str(data)}"
90
+
91
+ def trace(self, message: str, data: Optional[Dict[str, Any]] = None):
92
+ """Log trace level message"""
93
+ log_message = message + (self._format_data(data) if data else "")
94
+ self.logger.log(LogLevel.TRACE.value, log_message)
95
+
96
+ def debug(self, message: str, data: Optional[Dict[str, Any]] = None):
97
+ """Log debug level message"""
98
+ log_message = message + (self._format_data(data) if data else "")
99
+ self.logger.debug(log_message)
100
+
101
+ def info(self, message: str, data: Optional[Dict[str, Any]] = None):
102
+ """Log info level message"""
103
+ log_message = message + (self._format_data(data) if data else "")
104
+ self.logger.info(log_message)
105
+
106
+ def warn(self, message: str, data: Optional[Dict[str, Any]] = None):
107
+ """Log warning level message"""
108
+ log_message = message + (self._format_data(data) if data else "")
109
+ self.logger.warning(log_message)
110
+
111
+ def error(self, message: str, data: Optional[Dict[str, Any]] = None):
112
+ """Log error level message"""
113
+ log_message = message + (self._format_data(data) if data else "")
114
+ self.logger.error(log_message)
115
+
116
+ def log_operation(self, operation: str, data: Optional[Dict[str, Any]] = None):
117
+ """Log API operation with data"""
118
+ self.debug(f"Operation: {operation}", data)
119
+
120
+ def log_request(self, method: str, url: str, data: Optional[Dict[str, Any]] = None):
121
+ """Log HTTP request"""
122
+ request_data = {"method": method, "url": url}
123
+ if data:
124
+ request_data["data"] = data
125
+ self.debug("HTTP Request", request_data)
126
+
127
+ def log_response(self, status_code: int, url: str, data: Optional[Dict[str, Any]] = None):
128
+ """Log HTTP response"""
129
+ response_data = {"status": status_code, "url": url}
130
+ if data:
131
+ response_data["response"] = data
132
+ self.debug("HTTP Response", response_data)
133
+
134
+
135
+ # Global logger instances
136
+ _loggers: Dict[str, SiengeLogger] = {}
137
+
138
+
139
+ def get_logger(name: str = "SiengeMCP") -> SiengeLogger:
140
+ """Get or create a logger instance"""
141
+ if name not in _loggers:
142
+ _loggers[name] = SiengeLogger(name)
143
+ return _loggers[name]
144
+
145
+
146
+ # Convenience functions for default logger
147
+ def trace(message: str, data: Optional[Dict[str, Any]] = None):
148
+ """Log trace level message using default logger"""
149
+ get_logger().trace(message, data)
150
+
151
+
152
+ def debug(message: str, data: Optional[Dict[str, Any]] = None):
153
+ """Log debug level message using default logger"""
154
+ get_logger().debug(message, data)
155
+
156
+
157
+ def info(message: str, data: Optional[Dict[str, Any]] = None):
158
+ """Log info level message using default logger"""
159
+ get_logger().info(message, data)
160
+
161
+
162
+ def warn(message: str, data: Optional[Dict[str, Any]] = None):
163
+ """Log warning level message using default logger"""
164
+ get_logger().warn(message, data)
165
+
166
+
167
+ def error(message: str, data: Optional[Dict[str, Any]] = None):
168
+ """Log error level message using default logger"""
169
+ get_logger().error(message, data)