mcp-bitrix24 0.1.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.
mcp_bitrix24/__init__.py
ADDED
|
File without changes
|
mcp_bitrix24/client.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
import os
|
|
3
|
+
from datetime import date, timedelta
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BitrixClient:
|
|
8
|
+
def __init__(self, webhook_url: str | None = None):
|
|
9
|
+
self.webhook_url = (webhook_url or os.getenv("BITRIX24_WEBHOOK_URL", "")).rstrip("/")
|
|
10
|
+
if not self.webhook_url:
|
|
11
|
+
raise ValueError("BITRIX24_WEBHOOK_URL não configurado")
|
|
12
|
+
|
|
13
|
+
def _call(self, method: str, params: dict[str, Any] | None = None) -> Any:
|
|
14
|
+
url = f"{self.webhook_url}/{method}"
|
|
15
|
+
response = httpx.post(url, json=params or {}, timeout=30)
|
|
16
|
+
response.raise_for_status()
|
|
17
|
+
data = response.json()
|
|
18
|
+
if "error" in data:
|
|
19
|
+
raise RuntimeError(f"Bitrix24 error: {data.get('error_description', data['error'])}")
|
|
20
|
+
return data.get("result")
|
|
21
|
+
|
|
22
|
+
def list_deals(
|
|
23
|
+
self,
|
|
24
|
+
filter: dict | None = None,
|
|
25
|
+
select: list[str] | None = None,
|
|
26
|
+
order: dict | None = None,
|
|
27
|
+
start: int = 0,
|
|
28
|
+
) -> list[dict]:
|
|
29
|
+
params: dict[str, Any] = {"start": start}
|
|
30
|
+
if filter:
|
|
31
|
+
params["filter"] = filter
|
|
32
|
+
if select:
|
|
33
|
+
params["select"] = select
|
|
34
|
+
if order:
|
|
35
|
+
params["order"] = order
|
|
36
|
+
result = self._call("crm.deal.list", params)
|
|
37
|
+
return result if isinstance(result, list) else []
|
|
38
|
+
|
|
39
|
+
def get_deal(self, deal_id: int) -> dict:
|
|
40
|
+
return self._call("crm.deal.get", {"id": deal_id})
|
|
41
|
+
|
|
42
|
+
def create_deal(self, fields: dict) -> int:
|
|
43
|
+
return self._call("crm.deal.add", {"fields": fields})
|
|
44
|
+
|
|
45
|
+
def update_deal(self, deal_id: int, fields: dict) -> bool:
|
|
46
|
+
return self._call("crm.deal.update", {"id": deal_id, "fields": fields})
|
|
47
|
+
|
|
48
|
+
def get_stages(self, pipeline_id: int | None = None) -> list[dict]:
|
|
49
|
+
params: dict[str, Any] = {}
|
|
50
|
+
if pipeline_id is not None:
|
|
51
|
+
params["filter"] = {"CATEGORY_ID": pipeline_id}
|
|
52
|
+
return self._call("crm.dealcategory.stage.list", params) or []
|
|
53
|
+
|
|
54
|
+
def add_comment(self, deal_id: int, comment: str) -> int:
|
|
55
|
+
fields = {
|
|
56
|
+
"OWNER_TYPE_ID": 2,
|
|
57
|
+
"OWNER_ID": deal_id,
|
|
58
|
+
"TYPE_ID": 12,
|
|
59
|
+
"SUBJECT": "Nota",
|
|
60
|
+
"DESCRIPTION": comment,
|
|
61
|
+
"DESCRIPTION_TYPE": 1,
|
|
62
|
+
"COMPLETED": "Y",
|
|
63
|
+
"DIRECTION": 0,
|
|
64
|
+
}
|
|
65
|
+
return self._call("crm.activity.add", {"fields": fields})
|
|
66
|
+
|
|
67
|
+
def add_task(self, deal_id: int, subject: str, description: str = "", deadline: str = "") -> int:
|
|
68
|
+
fields: dict[str, Any] = {
|
|
69
|
+
"OWNER_TYPE_ID": 2,
|
|
70
|
+
"OWNER_ID": deal_id,
|
|
71
|
+
"TYPE_ID": 6,
|
|
72
|
+
"SUBJECT": subject,
|
|
73
|
+
"DESCRIPTION": description,
|
|
74
|
+
"COMPLETED": "N",
|
|
75
|
+
"DIRECTION": 0,
|
|
76
|
+
}
|
|
77
|
+
if deadline:
|
|
78
|
+
fields["DEADLINE"] = deadline
|
|
79
|
+
return self._call("crm.activity.add", {"fields": fields})
|
|
80
|
+
|
|
81
|
+
def list_pipelines(self) -> list[dict]:
|
|
82
|
+
return self._call("crm.dealcategory.list") or []
|
|
83
|
+
|
|
84
|
+
def list_tasks(
|
|
85
|
+
self,
|
|
86
|
+
responsible_id: int | None = None,
|
|
87
|
+
overdue_only: bool = False,
|
|
88
|
+
due_today: bool = False,
|
|
89
|
+
due_this_week: bool = False,
|
|
90
|
+
start: int = 0,
|
|
91
|
+
) -> list[dict]:
|
|
92
|
+
filter: dict[str, Any] = {"!STATUS": 5}
|
|
93
|
+
if responsible_id:
|
|
94
|
+
filter["RESPONSIBLE_ID"] = responsible_id
|
|
95
|
+
if overdue_only:
|
|
96
|
+
filter["<=DEADLINE"] = date.today().isoformat()
|
|
97
|
+
if due_today:
|
|
98
|
+
filter[">=DEADLINE"] = date.today().isoformat()
|
|
99
|
+
filter["<=DEADLINE"] = date.today().isoformat()
|
|
100
|
+
if due_this_week:
|
|
101
|
+
filter[">=DEADLINE"] = date.today().isoformat()
|
|
102
|
+
filter["<=DEADLINE"] = (date.today() + timedelta(days=7)).isoformat()
|
|
103
|
+
|
|
104
|
+
params: dict[str, Any] = {
|
|
105
|
+
"filter": filter,
|
|
106
|
+
"select": ["ID", "TITLE", "DEADLINE", "RESPONSIBLE_ID", "STATUS", "UF_CRM_TASK"],
|
|
107
|
+
"order": {"ID": "desc"},
|
|
108
|
+
"start": start,
|
|
109
|
+
}
|
|
110
|
+
result = self._call("tasks.task.list", params)
|
|
111
|
+
if isinstance(result, dict):
|
|
112
|
+
return result.get("tasks", [])
|
|
113
|
+
return []
|
|
114
|
+
|
|
115
|
+
def list_dormant_deals(self, days: int = 20) -> list[dict]:
|
|
116
|
+
cutoff = (date.today() - timedelta(days=days)).isoformat()
|
|
117
|
+
return self.list_deals(
|
|
118
|
+
filter={"<=DATE_MODIFY": cutoff, "CLOSED": "N"},
|
|
119
|
+
select=["ID", "TITLE", "STAGE_ID", "DATE_MODIFY", "ASSIGNED_BY_ID", "OPPORTUNITY", "CURRENCY_ID"],
|
|
120
|
+
order={"DATE_MODIFY": "ASC"},
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def list_closing_this_week(self) -> list[dict]:
|
|
124
|
+
today = date.today().isoformat()
|
|
125
|
+
next_week = (date.today() + timedelta(days=7)).isoformat()
|
|
126
|
+
return self.list_deals(
|
|
127
|
+
filter={">=CLOSEDATE": today, "<=CLOSEDATE": next_week, "CLOSED": "N"},
|
|
128
|
+
select=["ID", "TITLE", "STAGE_ID", "CLOSEDATE", "OPPORTUNITY", "CURRENCY_ID", "ASSIGNED_BY_ID"],
|
|
129
|
+
order={"CLOSEDATE": "ASC"},
|
|
130
|
+
)
|
mcp_bitrix24/server.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from dotenv import load_dotenv
|
|
4
|
+
from mcp.server.fastmcp import FastMCP
|
|
5
|
+
from mcp_bitrix24.client import BitrixClient
|
|
6
|
+
|
|
7
|
+
load_dotenv()
|
|
8
|
+
|
|
9
|
+
mcp = FastMCP("Bitrix24 CRM")
|
|
10
|
+
client = BitrixClient()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@mcp.tool()
|
|
14
|
+
def list_deals(
|
|
15
|
+
stage: str = "",
|
|
16
|
+
responsible_id: int = 0,
|
|
17
|
+
title_contains: str = "",
|
|
18
|
+
limit: int = 20,
|
|
19
|
+
) -> str:
|
|
20
|
+
"""Lista deals do Bitrix24. Filtre por estágio, responsável ou texto no título."""
|
|
21
|
+
filter: dict = {}
|
|
22
|
+
if stage:
|
|
23
|
+
filter["STAGE_ID"] = stage
|
|
24
|
+
if responsible_id:
|
|
25
|
+
filter["ASSIGNED_BY_ID"] = responsible_id
|
|
26
|
+
if title_contains:
|
|
27
|
+
filter["%TITLE"] = title_contains
|
|
28
|
+
|
|
29
|
+
select = ["ID", "TITLE", "STAGE_ID", "OPPORTUNITY", "CURRENCY_ID",
|
|
30
|
+
"ASSIGNED_BY_ID", "DATE_CREATE", "CLOSEDATE", "COMMENTS"]
|
|
31
|
+
|
|
32
|
+
deals = client.list_deals(filter=filter or None, select=select)
|
|
33
|
+
deals = deals[:limit]
|
|
34
|
+
|
|
35
|
+
if not deals:
|
|
36
|
+
return "Nenhum deal encontrado com os filtros fornecidos."
|
|
37
|
+
|
|
38
|
+
lines = [f"Encontrados {len(deals)} deals:\n"]
|
|
39
|
+
for d in deals:
|
|
40
|
+
valor = f"{d.get('OPPORTUNITY', '0')} {d.get('CURRENCY_ID', '')}"
|
|
41
|
+
lines.append(
|
|
42
|
+
f"• [{d['ID']}] {d['TITLE']}\n"
|
|
43
|
+
f" Estágio: {d.get('STAGE_ID')} | Valor: {valor}\n"
|
|
44
|
+
f" Responsável ID: {d.get('ASSIGNED_BY_ID')} | Fechamento: {d.get('CLOSEDATE', 'N/A')}"
|
|
45
|
+
)
|
|
46
|
+
return "\n".join(lines)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@mcp.tool()
|
|
50
|
+
def get_deal(deal_id: int) -> str:
|
|
51
|
+
"""Retorna todos os detalhes de um deal específico pelo ID."""
|
|
52
|
+
deal = client.get_deal(deal_id)
|
|
53
|
+
if not deal:
|
|
54
|
+
return f"Deal {deal_id} não encontrado."
|
|
55
|
+
return json.dumps(deal, ensure_ascii=False, indent=2)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@mcp.tool()
|
|
59
|
+
def create_deal(
|
|
60
|
+
title: str,
|
|
61
|
+
stage_id: str = "",
|
|
62
|
+
value: float = 0.0,
|
|
63
|
+
currency: str = "BRL",
|
|
64
|
+
contact_id: int = 0,
|
|
65
|
+
company_id: int = 0,
|
|
66
|
+
responsible_id: int = 0,
|
|
67
|
+
comments: str = "",
|
|
68
|
+
close_date: str = "",
|
|
69
|
+
) -> str:
|
|
70
|
+
"""
|
|
71
|
+
Cria um novo deal no Bitrix24.
|
|
72
|
+
close_date deve estar no formato YYYY-MM-DD.
|
|
73
|
+
"""
|
|
74
|
+
fields: dict = {"TITLE": title, "CURRENCY_ID": currency}
|
|
75
|
+
if stage_id:
|
|
76
|
+
fields["STAGE_ID"] = stage_id
|
|
77
|
+
if value:
|
|
78
|
+
fields["OPPORTUNITY"] = value
|
|
79
|
+
if contact_id:
|
|
80
|
+
fields["CONTACT_ID"] = contact_id
|
|
81
|
+
if company_id:
|
|
82
|
+
fields["COMPANY_ID"] = company_id
|
|
83
|
+
if responsible_id:
|
|
84
|
+
fields["ASSIGNED_BY_ID"] = responsible_id
|
|
85
|
+
if comments:
|
|
86
|
+
fields["COMMENTS"] = comments
|
|
87
|
+
if close_date:
|
|
88
|
+
fields["CLOSEDATE"] = f"{close_date}T00:00:00+03:00"
|
|
89
|
+
|
|
90
|
+
new_id = client.create_deal(fields)
|
|
91
|
+
return f"Deal criado com sucesso! ID: {new_id}"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@mcp.tool()
|
|
95
|
+
def update_deal(deal_id: int, fields_json: str) -> str:
|
|
96
|
+
"""
|
|
97
|
+
Atualiza campos de um deal existente.
|
|
98
|
+
fields_json deve ser um JSON com os campos a atualizar.
|
|
99
|
+
Exemplo: {"TITLE": "Novo nome", "OPPORTUNITY": 5000}
|
|
100
|
+
"""
|
|
101
|
+
try:
|
|
102
|
+
fields = json.loads(fields_json)
|
|
103
|
+
except json.JSONDecodeError as e:
|
|
104
|
+
return f"Erro: fields_json inválido — {e}"
|
|
105
|
+
|
|
106
|
+
client.update_deal(deal_id, fields)
|
|
107
|
+
return f"Deal {deal_id} atualizado com sucesso."
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@mcp.tool()
|
|
111
|
+
def move_deal_stage(deal_id: int, stage_id: str) -> str:
|
|
112
|
+
"""
|
|
113
|
+
Move um deal para um estágio do funil.
|
|
114
|
+
Use list_stages para ver os estágios disponíveis.
|
|
115
|
+
"""
|
|
116
|
+
client.update_deal(deal_id, {"STAGE_ID": stage_id})
|
|
117
|
+
return f"Deal {deal_id} movido para o estágio '{stage_id}'."
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@mcp.tool()
|
|
121
|
+
def list_stages(pipeline_id: int = 0) -> str:
|
|
122
|
+
"""Lista os estágios disponíveis no funil. pipeline_id=0 lista o funil padrão."""
|
|
123
|
+
stages = client.get_stages(pipeline_id if pipeline_id else None)
|
|
124
|
+
if not stages:
|
|
125
|
+
return "Nenhum estágio encontrado."
|
|
126
|
+
lines = ["Estágios disponíveis:"]
|
|
127
|
+
for s in stages:
|
|
128
|
+
lines.append(f"• {s.get('STATUS_ID')} — {s.get('NAME')}")
|
|
129
|
+
return "\n".join(lines)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@mcp.tool()
|
|
133
|
+
def add_comment(deal_id: int, comment: str) -> str:
|
|
134
|
+
"""Adiciona uma nota/comentário a um deal."""
|
|
135
|
+
activity_id = client.add_comment(deal_id, comment)
|
|
136
|
+
return f"Comentário adicionado ao deal {deal_id}. Atividade ID: {activity_id}"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@mcp.tool()
|
|
140
|
+
def add_task(deal_id: int, subject: str, description: str = "", deadline: str = "") -> str:
|
|
141
|
+
"""
|
|
142
|
+
Adiciona uma tarefa a um deal.
|
|
143
|
+
deadline deve estar no formato YYYY-MM-DDTHH:MM:SS+HH:MM.
|
|
144
|
+
"""
|
|
145
|
+
activity_id = client.add_task(deal_id, subject, description, deadline)
|
|
146
|
+
return f"Tarefa '{subject}' adicionada ao deal {deal_id}. Atividade ID: {activity_id}"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@mcp.tool()
|
|
150
|
+
def list_pipelines() -> str:
|
|
151
|
+
"""Lista todos os funis (pipelines) de deals disponíveis."""
|
|
152
|
+
pipelines = client.list_pipelines()
|
|
153
|
+
if not pipelines:
|
|
154
|
+
return "Apenas o funil padrão (ID: 0) está disponível."
|
|
155
|
+
lines = ["Funis disponíveis:"]
|
|
156
|
+
for p in pipelines:
|
|
157
|
+
lines.append(f"• ID: {p.get('ID')} — {p.get('NAME')}")
|
|
158
|
+
return "\n".join(lines)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@mcp.tool()
|
|
162
|
+
def list_tasks(
|
|
163
|
+
responsible_id: int = 0,
|
|
164
|
+
overdue_only: bool = False,
|
|
165
|
+
due_this_week: bool = False,
|
|
166
|
+
limit: int = 20,
|
|
167
|
+
) -> str:
|
|
168
|
+
"""Lista tarefas pendentes do Bitrix24. overdue_only=true retorna só as atrasadas. due_this_week=true retorna tarefas com prazo nos próximos 7 dias."""
|
|
169
|
+
tasks = client.list_tasks(
|
|
170
|
+
responsible_id=responsible_id or None,
|
|
171
|
+
overdue_only=overdue_only,
|
|
172
|
+
due_this_week=due_this_week,
|
|
173
|
+
)
|
|
174
|
+
tasks = tasks[:limit]
|
|
175
|
+
|
|
176
|
+
if not tasks:
|
|
177
|
+
return "Nenhuma tarefa encontrada."
|
|
178
|
+
|
|
179
|
+
lines = [f"Encontradas {len(tasks)} tarefas:\n"]
|
|
180
|
+
for t in tasks:
|
|
181
|
+
deadline = t.get("deadline") or t.get("DEADLINE") or "sem prazo"
|
|
182
|
+
crm_link = t.get("ufCrmTask") or t.get("UF_CRM_TASK") or ""
|
|
183
|
+
lines.append(
|
|
184
|
+
f"• [{t.get('id') or t.get('ID')}] {t.get('title') or t.get('TITLE')}\n"
|
|
185
|
+
f" Prazo: {deadline} | Status: {t.get('status') or t.get('STATUS')}\n"
|
|
186
|
+
f" Responsável ID: {t.get('responsibleId') or t.get('RESPONSIBLE_ID')} | CRM: {crm_link}"
|
|
187
|
+
)
|
|
188
|
+
return "\n".join(lines)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@mcp.tool()
|
|
192
|
+
def daily_briefing(dormant_days: int = 20) -> str:
|
|
193
|
+
"""
|
|
194
|
+
Gera um briefing consolidado do dia: tarefas atrasadas, tarefas para hoje,
|
|
195
|
+
deals dormentes e deals com fechamento previsto para esta semana.
|
|
196
|
+
dormant_days: quantos dias sem atividade para considerar um deal dormente (padrão: 20).
|
|
197
|
+
"""
|
|
198
|
+
from datetime import date
|
|
199
|
+
today = date.today().strftime("%d/%m/%Y")
|
|
200
|
+
sections = [f"BRIEFING DO DIA — {today}\n"]
|
|
201
|
+
|
|
202
|
+
# Tarefas atrasadas
|
|
203
|
+
overdue = client.list_tasks(overdue_only=True)
|
|
204
|
+
if overdue:
|
|
205
|
+
sections.append(f"TAREFAS ATRASADAS ({len(overdue)})")
|
|
206
|
+
for t in overdue[:10]:
|
|
207
|
+
deal = (t.get("ufCrmTask") or [""])[0]
|
|
208
|
+
deadline = (t.get("deadline") or "sem prazo")[:10]
|
|
209
|
+
sections.append(f" • [{t.get('id')}] {t.get('title')} | prazo: {deadline} | deal: {deal}")
|
|
210
|
+
else:
|
|
211
|
+
sections.append("TAREFAS ATRASADAS\n Nenhuma.")
|
|
212
|
+
|
|
213
|
+
sections.append("")
|
|
214
|
+
|
|
215
|
+
# Tarefas para hoje
|
|
216
|
+
today_tasks = client.list_tasks(due_today=True)
|
|
217
|
+
if today_tasks:
|
|
218
|
+
sections.append(f"TAREFAS PARA HOJE ({len(today_tasks)})")
|
|
219
|
+
for t in today_tasks[:10]:
|
|
220
|
+
deal = (t.get("ufCrmTask") or [""])[0]
|
|
221
|
+
sections.append(f" • [{t.get('id')}] {t.get('title')} | deal: {deal}")
|
|
222
|
+
else:
|
|
223
|
+
sections.append("TAREFAS PARA HOJE\n Nenhuma.")
|
|
224
|
+
|
|
225
|
+
sections.append("")
|
|
226
|
+
|
|
227
|
+
# Deals dormentes
|
|
228
|
+
dormant = client.list_dormant_deals(days=dormant_days)
|
|
229
|
+
if dormant:
|
|
230
|
+
sections.append(f"DEALS DORMENTES — sem atividade há +{dormant_days} dias ({len(dormant)})")
|
|
231
|
+
for d in dormant[:10]:
|
|
232
|
+
modified = (d.get("DATE_MODIFY") or "")[:10]
|
|
233
|
+
valor = f"{d.get('OPPORTUNITY', '0')} {d.get('CURRENCY_ID', '')}"
|
|
234
|
+
sections.append(f" • [{d['ID']}] {d['TITLE']} | estágio: {d.get('STAGE_ID')} | último update: {modified} | valor: {valor}")
|
|
235
|
+
else:
|
|
236
|
+
sections.append(f"DEALS DORMENTES\n Nenhum deal parado há mais de {dormant_days} dias.")
|
|
237
|
+
|
|
238
|
+
sections.append("")
|
|
239
|
+
|
|
240
|
+
# Forecast da semana
|
|
241
|
+
closing = client.list_closing_this_week()
|
|
242
|
+
if closing:
|
|
243
|
+
sections.append(f"FECHAMENTO PREVISTO ESTA SEMANA ({len(closing)})")
|
|
244
|
+
for d in closing[:10]:
|
|
245
|
+
closedate = (d.get("CLOSEDATE") or "")[:10]
|
|
246
|
+
valor = f"{d.get('OPPORTUNITY', '0')} {d.get('CURRENCY_ID', '')}"
|
|
247
|
+
sections.append(f" • [{d['ID']}] {d['TITLE']} | fechamento: {closedate} | valor: {valor}")
|
|
248
|
+
else:
|
|
249
|
+
sections.append("FECHAMENTO PREVISTO ESTA SEMANA\n Nenhum deal.")
|
|
250
|
+
|
|
251
|
+
return "\n".join(sections)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def main():
|
|
255
|
+
mcp.run(transport="stdio")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
if __name__ == "__main__":
|
|
259
|
+
main()
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcp-bitrix24
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP Server for Bitrix24 CRM — manage deals via Claude
|
|
5
|
+
Project-URL: Homepage, https://github.com/lucbrito88-boop/mcp-bitrix24
|
|
6
|
+
Project-URL: Repository, https://github.com/lucbrito88-boop/mcp-bitrix24
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: ai,bitrix24,claude,crm,mcp
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Requires-Dist: httpx>=0.27.0
|
|
11
|
+
Requires-Dist: mcp[cli]>=1.0.0
|
|
12
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Bitrix24 MCP Server
|
|
16
|
+
|
|
17
|
+
MCP Server que integra o CRM Bitrix24 ao Claude, permitindo gerenciar deals diretamente via linguagem natural.
|
|
18
|
+
|
|
19
|
+
## O que você pode fazer
|
|
20
|
+
|
|
21
|
+
Após configurar, você pode pedir ao Claude coisas como:
|
|
22
|
+
|
|
23
|
+
- *"Liste meus deals em aberto"*
|
|
24
|
+
- *"Mostre os detalhes do deal 123"*
|
|
25
|
+
- *"Crie um deal chamado 'Proposta NTSec' no valor de R$ 50.000"*
|
|
26
|
+
- *"Mova o deal 456 para a etapa de negociação"*
|
|
27
|
+
- *"Adicione uma nota no deal 789: reunião realizada com sucesso"*
|
|
28
|
+
- *"Quais são os estágios do meu funil?"*
|
|
29
|
+
- *"Liste minhas tarefas atrasadas"*
|
|
30
|
+
- *"Quais tarefas estão pendentes para o responsável 42?"*
|
|
31
|
+
|
|
32
|
+
## Ferramentas disponíveis
|
|
33
|
+
|
|
34
|
+
| Ferramenta | Descrição |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `list_deals` | Lista deals com filtros por estágio, responsável ou título |
|
|
37
|
+
| `get_deal` | Retorna todos os detalhes de um deal pelo ID |
|
|
38
|
+
| `create_deal` | Cria um novo deal no CRM |
|
|
39
|
+
| `update_deal` | Atualiza campos de um deal existente |
|
|
40
|
+
| `move_deal_stage` | Move um deal para outro estágio do funil |
|
|
41
|
+
| `list_stages` | Lista os estágios disponíveis no funil |
|
|
42
|
+
| `add_comment` | Adiciona nota/comentário a um deal |
|
|
43
|
+
| `add_task` | Cria uma tarefa vinculada a um deal |
|
|
44
|
+
| `list_tasks` | Lista tarefas pendentes; filtra por responsável ou só atrasadas |
|
|
45
|
+
| `list_pipelines` | Lista os funis de deals disponíveis |
|
|
46
|
+
|
|
47
|
+
## Pré-requisitos
|
|
48
|
+
|
|
49
|
+
- Python 3.10+
|
|
50
|
+
- [uv](https://docs.astral.sh/uv/) (gerenciador de pacotes)
|
|
51
|
+
- Claude Code CLI
|
|
52
|
+
- Conta no Bitrix24 com acesso à API (webhook configurado)
|
|
53
|
+
|
|
54
|
+
## Instalação
|
|
55
|
+
|
|
56
|
+
### 1. Clone o repositório
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
git clone https://github.com/seu-usuario/bitrix24-mcp.git
|
|
60
|
+
cd bitrix24-mcp
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 2. Crie o ambiente virtual e instale as dependências
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
uv venv .venv
|
|
67
|
+
uv pip install -r requirements.txt
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 3. Configure o webhook do Bitrix24
|
|
71
|
+
|
|
72
|
+
Copie o arquivo de exemplo e adicione sua URL:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
cp .env.example .env
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Edite o `.env`:
|
|
79
|
+
|
|
80
|
+
```env
|
|
81
|
+
BITRIX24_WEBHOOK_URL=https://seudominio.bitrix24.com.br/rest/USER_ID/WEBHOOK_TOKEN/
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Como obter o webhook no Bitrix24:**
|
|
85
|
+
1. Acesse seu Bitrix24 → Configurações → Integrações → Webhooks de entrada
|
|
86
|
+
2. Crie um webhook com permissões de CRM (leitura e escrita)
|
|
87
|
+
3. Copie a URL gerada
|
|
88
|
+
|
|
89
|
+
### 4. Configure o MCP Server no Claude Code
|
|
90
|
+
|
|
91
|
+
Adicione ao seu `.claude/settings.json` (ou `~/.claude/settings.json` para uso global):
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"mcpServers": {
|
|
96
|
+
"bitrix24": {
|
|
97
|
+
"command": "/caminho/para/bitrix24-mcp/.venv/Scripts/python.exe",
|
|
98
|
+
"args": ["/caminho/para/bitrix24-mcp/server.py"]
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
> **Windows:** use `.venv\Scripts\python.exe`
|
|
105
|
+
> **Mac/Linux:** use `.venv/bin/python`
|
|
106
|
+
|
|
107
|
+
### 5. Reinicie o Claude Code
|
|
108
|
+
|
|
109
|
+
Feche e reabra o Claude Code. O MCP server `bitrix24` deve aparecer disponível.
|
|
110
|
+
|
|
111
|
+
## Estrutura do projeto
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
bitrix24-mcp/
|
|
115
|
+
├── src/mcp_bitrix24/
|
|
116
|
+
│ ├── server.py # MCP server — define as ferramentas expostas ao Claude
|
|
117
|
+
│ └── client.py # Wrapper da API REST do Bitrix24
|
|
118
|
+
├── pyproject.toml # Configuração do pacote (PyPI)
|
|
119
|
+
├── requirements.txt # Dependências Python
|
|
120
|
+
├── .env.example # Template de configuração
|
|
121
|
+
└── .gitignore
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Dependências
|
|
125
|
+
|
|
126
|
+
- [`mcp`](https://github.com/anthropics/mcp) — SDK do Model Context Protocol
|
|
127
|
+
- [`httpx`](https://www.python-httpx.org/) — cliente HTTP assíncrono
|
|
128
|
+
- [`python-dotenv`](https://github.com/theskumar/python-dotenv) — carregamento do `.env`
|
|
129
|
+
|
|
130
|
+
## Segurança
|
|
131
|
+
|
|
132
|
+
- O arquivo `.env` está no `.gitignore` — nunca commite suas credenciais
|
|
133
|
+
- O webhook do Bitrix24 deve ter permissões de CRM e Tarefas (task)
|
|
134
|
+
- Recomenda-se criar um usuário de serviço dedicado no Bitrix24 para o webhook
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
mcp_bitrix24/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
mcp_bitrix24/client.py,sha256=VJnYNDM2SOOS8wzwCW-bOVytz7BRSYejYB5uzvLsol0,4909
|
|
3
|
+
mcp_bitrix24/server.py,sha256=fPFP411ieQF_h7lHR9977hdNniCL-Z66X4fZKBScPRM,8651
|
|
4
|
+
mcp_bitrix24-0.1.0.dist-info/METADATA,sha256=P1R8iBaypVtTMkYR6ls76JwyPzfLHBmNZhpPj4aZBvw,4124
|
|
5
|
+
mcp_bitrix24-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
6
|
+
mcp_bitrix24-0.1.0.dist-info/entry_points.txt,sha256=MyLb0Z8DmN_V00LsrmrA6l8IZ7LIHGzgkACS_RLn1PY,58
|
|
7
|
+
mcp_bitrix24-0.1.0.dist-info/RECORD,,
|