mcp-chatwoot 0.2.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
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.
package/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # mcp-chatwoot
2
+
3
+ MCP server **read-only** para Chatwoot — lista conversas, lê mensagens e busca em qualquer inbox (WhatsApp, e-mail, API, etc).
4
+
5
+ Funciona em qualquer client MCP: **Claude Code, Cursor, VS Code (Copilot/Cline/Continue), Windsurf, Antigravity, Zed**.
6
+
7
+ Dois transportes: **stdio** (default, recomendado) e **HTTP** (com bearer token opcional pra expor pra rede).
8
+
9
+ ---
10
+
11
+ ## Tools
12
+
13
+ | Tool | Descrição |
14
+ |---|---|
15
+ | `list_conversations` | Lista conversas (filtros: status, page, labels, inbox_id). |
16
+ | `read_conversation_messages` | Mensagens de uma conversa (texto, autor, timestamp, anexos). |
17
+ | `search_messages` | Busca full-text em mensagens. |
18
+ | `summarize_conversation` | Resumo: participantes, contagem por autor, primeira/última mensagem, anexos. |
19
+
20
+ ---
21
+
22
+ ## Variáveis de ambiente
23
+
24
+ | Var | Obrigatório | Default | Descrição |
25
+ |---|---|---|---|
26
+ | `CHATWOOT_URL` | sim | `http://localhost:3000` | URL do Chatwoot. |
27
+ | `CHATWOOT_TOKEN` | sim | — | API access token (em Profile → Access Token). |
28
+ | `CHATWOOT_ACCOUNT_ID` | não | `1` | ID da conta. |
29
+ | `CHATWOOT_INBOX_ID` | não | — | Trava o MCP em um inbox. Vazio = toda a conta. |
30
+ | `CHATWOOT_INBOX_LABEL` | não | — | Apenas pra documentar (ex: "Techmax WhatsApp"). |
31
+ | `MCP_TRANSPORT` | não | `stdio` | `stdio` ou `http`. |
32
+ | `PORT` | não | `8765` | Porta HTTP (se `MCP_TRANSPORT=http`). |
33
+ | `MCP_AUTH_TOKEN` | não | — | Se setado, exige `Authorization: Bearer <token>` (só HTTP). |
34
+
35
+ ---
36
+
37
+ ## Instalação
38
+
39
+ ### Claude Code
40
+
41
+ ```bash
42
+ claude mcp add chatwoot \
43
+ -e CHATWOOT_URL=https://seu-chatwoot.com \
44
+ -e CHATWOOT_TOKEN=SEU_TOKEN \
45
+ -e CHATWOOT_INBOX_ID=2 \
46
+ -- npx -y mcp-chatwoot
47
+ ```
48
+
49
+ ### Cursor
50
+
51
+ `~/.cursor/mcp.json`:
52
+
53
+ ```json
54
+ {
55
+ "mcpServers": {
56
+ "chatwoot": {
57
+ "command": "npx",
58
+ "args": ["-y", "mcp-chatwoot"],
59
+ "env": {
60
+ "CHATWOOT_URL": "https://seu-chatwoot.com",
61
+ "CHATWOOT_TOKEN": "SEU_TOKEN",
62
+ "CHATWOOT_INBOX_ID": "2"
63
+ }
64
+ }
65
+ }
66
+ }
67
+ ```
68
+
69
+ ### VS Code (extensões com MCP: Cline, Continue, Copilot)
70
+
71
+ Em `.vscode/mcp.json` ou settings da extensão:
72
+
73
+ ```json
74
+ {
75
+ "servers": {
76
+ "chatwoot": {
77
+ "type": "stdio",
78
+ "command": "npx",
79
+ "args": ["-y", "mcp-chatwoot"],
80
+ "env": {
81
+ "CHATWOOT_URL": "https://seu-chatwoot.com",
82
+ "CHATWOOT_TOKEN": "SEU_TOKEN"
83
+ }
84
+ }
85
+ }
86
+ }
87
+ ```
88
+
89
+ ### Windsurf / Antigravity / Zed
90
+
91
+ Mesma config de stdio do Cursor — adapte o arquivo de config do client (geralmente `mcp.json` ou painel de MCP servers nas settings).
92
+
93
+ ### Docker (HTTP transport)
94
+
95
+ ```bash
96
+ docker run -d \
97
+ --name mcp-chatwoot \
98
+ -p 8765:8765 \
99
+ -e MCP_TRANSPORT=http \
100
+ -e MCP_AUTH_TOKEN=$(openssl rand -hex 32) \
101
+ -e CHATWOOT_URL=https://seu-chatwoot.com \
102
+ -e CHATWOOT_TOKEN=SEU_TOKEN \
103
+ -e CHATWOOT_INBOX_ID=2 \
104
+ ghcr.io/REPLACE_ME/mcp-chatwoot:latest
105
+ ```
106
+
107
+ Depois no client (Claude Code):
108
+
109
+ ```bash
110
+ claude mcp add --transport http chatwoot http://SERVER:8765/mcp \
111
+ --header "Authorization: Bearer SEU_MCP_AUTH_TOKEN"
112
+ ```
113
+
114
+ ---
115
+
116
+ ## Rodar local sem instalar
117
+
118
+ ```bash
119
+ git clone <repo>
120
+ cd mcp-chatwoot
121
+ npm install
122
+ npm run build
123
+ CHATWOOT_URL=... CHATWOOT_TOKEN=... node dist/index.js
124
+ ```
125
+
126
+ ---
127
+
128
+ ## Licença
129
+
130
+ MIT
@@ -0,0 +1,47 @@
1
+ const CHATWOOT_URL = (process.env.CHATWOOT_URL ?? "http://localhost:3000").replace(/\/$/, "");
2
+ const CHATWOOT_TOKEN = process.env.CHATWOOT_TOKEN ?? "";
3
+ const ACCOUNT_ID = process.env.CHATWOOT_ACCOUNT_ID ?? "1";
4
+ const RAW_INBOX = process.env.CHATWOOT_INBOX_ID ?? "";
5
+ export const CHATWOOT_INBOX_ID = RAW_INBOX ? Number(RAW_INBOX) : undefined;
6
+ export const CHATWOOT_INBOX_LABEL = process.env.CHATWOOT_INBOX_LABEL ?? "";
7
+ if (!CHATWOOT_TOKEN) {
8
+ process.stderr.write("[mcp-chatwoot] WARN: CHATWOOT_TOKEN not set\n");
9
+ }
10
+ const BASE = `${CHATWOOT_URL}/api/v1/accounts/${ACCOUNT_ID}`;
11
+ async function cw(path, params) {
12
+ const url = new URL(`${BASE}${path}`);
13
+ if (params) {
14
+ for (const [k, v] of Object.entries(params)) {
15
+ if (v !== undefined)
16
+ url.searchParams.set(k, String(v));
17
+ }
18
+ }
19
+ const res = await fetch(url.toString(), {
20
+ headers: { api_access_token: CHATWOOT_TOKEN, "Content-Type": "application/json" },
21
+ });
22
+ if (!res.ok) {
23
+ throw new Error(`Chatwoot ${res.status}: ${await res.text()}`);
24
+ }
25
+ return res.json();
26
+ }
27
+ export async function listConversations(opts) {
28
+ const inbox = CHATWOOT_INBOX_ID ?? opts.inboxOverride;
29
+ const data = await cw("/conversations", {
30
+ inbox_id: inbox,
31
+ status: opts.status ?? "all",
32
+ page: opts.page ?? 1,
33
+ labels: opts.labels?.join(","),
34
+ });
35
+ return data.data;
36
+ }
37
+ export async function getConversationMessages(conversationId) {
38
+ const data = await cw(`/conversations/${conversationId}/messages`);
39
+ return data.payload;
40
+ }
41
+ export async function getConversation(conversationId) {
42
+ return cw(`/conversations/${conversationId}`);
43
+ }
44
+ export async function searchMessages(q, page = 1) {
45
+ const data = await cw("/conversations/search", { q, page });
46
+ return data.payload;
47
+ }
package/dist/index.js ADDED
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
5
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
6
+ import { z } from "zod";
7
+ import { listConversations, getConversationMessages, getConversation, searchMessages, CHATWOOT_INBOX_ID, CHATWOOT_INBOX_LABEL, } from "./chatwoot.js";
8
+ const TRANSPORT = (process.env.MCP_TRANSPORT ?? "stdio").toLowerCase();
9
+ const PORT = Number(process.env.PORT ?? 8765);
10
+ const AUTH_TOKEN = process.env.MCP_AUTH_TOKEN;
11
+ function buildServer() {
12
+ const server = new McpServer({
13
+ name: "chatwoot",
14
+ version: "0.2.0",
15
+ });
16
+ const inboxNote = CHATWOOT_INBOX_ID
17
+ ? `Escopo fixo: inbox_id=${CHATWOOT_INBOX_ID}${CHATWOOT_INBOX_LABEL ? ` (${CHATWOOT_INBOX_LABEL})` : ""}.`
18
+ : "Escopo: toda a conta Chatwoot (configure CHATWOOT_INBOX_ID pra restringir).";
19
+ server.registerTool("list_conversations", {
20
+ description: `Lista conversas do Chatwoot. ${inboxNote}`,
21
+ inputSchema: {
22
+ status: z.enum(["open", "resolved", "pending", "snoozed", "all"]).optional(),
23
+ page: z.number().int().min(1).optional(),
24
+ labels: z.array(z.string()).optional(),
25
+ inbox_id: z
26
+ .number()
27
+ .int()
28
+ .optional()
29
+ .describe("Override do inbox (ignorado se CHATWOOT_INBOX_ID estiver setado no servidor)."),
30
+ },
31
+ }, async ({ status, page, labels, inbox_id }) => {
32
+ const data = await listConversations({ status, page, labels, inboxOverride: inbox_id });
33
+ const slim = data.payload.map((c) => ({
34
+ id: c.id,
35
+ status: c.status,
36
+ inbox_id: c.inbox_id,
37
+ contact: c.meta?.sender?.name ?? c.meta?.sender?.phone_number ?? c.meta?.sender?.identifier,
38
+ last_message: c.last_non_activity_message?.content?.slice(0, 200),
39
+ last_at: c.last_non_activity_message?.created_at,
40
+ labels: c.labels,
41
+ }));
42
+ return {
43
+ content: [{ type: "text", text: JSON.stringify({ meta: data.meta, conversations: slim }, null, 2) }],
44
+ };
45
+ });
46
+ server.registerTool("read_conversation_messages", {
47
+ description: "Lê todas as mensagens de uma conversa (texto, autor, timestamp, anexos).",
48
+ inputSchema: {
49
+ conversation_id: z.number().int(),
50
+ only_text: z.boolean().optional().describe("Se true, oculta atividades do sistema."),
51
+ },
52
+ }, async ({ conversation_id, only_text }) => {
53
+ const msgs = await getConversationMessages(conversation_id);
54
+ const filtered = only_text ? msgs.filter((m) => m.message_type !== 2) : msgs;
55
+ return {
56
+ content: [{ type: "text", text: JSON.stringify(filtered.map(formatMessage), null, 2) }],
57
+ };
58
+ });
59
+ server.registerTool("search_messages", {
60
+ description: `Busca full-text em mensagens do Chatwoot. ${CHATWOOT_INBOX_ID ? `Resultado filtrado pelo inbox=${CHATWOOT_INBOX_ID}.` : "Sem filtro de inbox."}`,
61
+ inputSchema: {
62
+ q: z.string().min(2),
63
+ page: z.number().int().min(1).optional(),
64
+ },
65
+ }, async ({ q, page }) => {
66
+ const result = await searchMessages(q, page ?? 1);
67
+ const conversations = CHATWOOT_INBOX_ID
68
+ ? result.conversations.filter((c) => c.inbox_id === CHATWOOT_INBOX_ID)
69
+ : result.conversations;
70
+ return {
71
+ content: [
72
+ { type: "text", text: JSON.stringify({ conversations, messages: result.messages }, null, 2) },
73
+ ],
74
+ };
75
+ });
76
+ server.registerTool("summarize_conversation", {
77
+ description: "Resumo estruturado de uma conversa: participantes, total de mensagens, primeira/última, anexos.",
78
+ inputSchema: { conversation_id: z.number().int() },
79
+ }, async ({ conversation_id }) => {
80
+ const [conv, msgs] = await Promise.all([
81
+ getConversation(conversation_id),
82
+ getConversationMessages(conversation_id),
83
+ ]);
84
+ const real = msgs.filter((m) => m.message_type !== 2);
85
+ const bySender = new Map();
86
+ for (const m of real) {
87
+ const k = m.sender?.name ?? m.sender?.type ?? "desconhecido";
88
+ bySender.set(k, (bySender.get(k) ?? 0) + 1);
89
+ }
90
+ const first = real[0];
91
+ const last = real[real.length - 1];
92
+ const attachments = real.flatMap((m) => m.attachments ?? []);
93
+ return {
94
+ content: [
95
+ {
96
+ type: "text",
97
+ text: JSON.stringify({
98
+ conversation_id: conv.id,
99
+ status: conv.status,
100
+ inbox_id: conv.inbox_id,
101
+ contact: conv.meta?.sender,
102
+ total_messages: real.length,
103
+ participants: Array.from(bySender, ([name, count]) => ({ name, count })),
104
+ first_message: first ? formatMessage(first) : null,
105
+ last_message: last ? formatMessage(last) : null,
106
+ attachments_count: attachments.length,
107
+ attachment_types: Array.from(new Set(attachments.map((a) => a.file_type))),
108
+ }, null, 2),
109
+ },
110
+ ],
111
+ };
112
+ });
113
+ return server;
114
+ }
115
+ function formatMessage(m) {
116
+ const direction = m.message_type === 0 ? "in" : m.message_type === 1 ? "out" : "sys";
117
+ return {
118
+ id: m.id,
119
+ at: new Date(m.created_at * 1000).toISOString(),
120
+ direction,
121
+ from: m.sender?.name ?? m.sender?.type ?? null,
122
+ phone: m.sender?.phone_number ?? null,
123
+ content_type: m.content_type,
124
+ text: m.content,
125
+ attachments: (m.attachments ?? []).map((a) => ({ type: a.file_type, url: a.data_url })),
126
+ };
127
+ }
128
+ async function runStdio() {
129
+ const transport = new StdioServerTransport();
130
+ await buildServer().connect(transport);
131
+ process.stderr.write(`[mcp-chatwoot] stdio ready (inbox=${CHATWOOT_INBOX_ID ?? "all"})\n`);
132
+ }
133
+ async function runHttp() {
134
+ const { default: express } = await import("express");
135
+ const { randomUUID } = await import("node:crypto");
136
+ const app = express();
137
+ app.use(express.json({ limit: "4mb" }));
138
+ if (AUTH_TOKEN) {
139
+ app.use((req, res, next) => {
140
+ if (req.path === "/health")
141
+ return next();
142
+ const h = req.headers.authorization ?? "";
143
+ if (h !== `Bearer ${AUTH_TOKEN}`) {
144
+ res.status(401).json({ error: "unauthorized" });
145
+ return;
146
+ }
147
+ next();
148
+ });
149
+ }
150
+ const transports = new Map();
151
+ app.post("/mcp", async (req, res) => {
152
+ const sid = req.headers["mcp-session-id"];
153
+ let transport = sid ? transports.get(sid) : undefined;
154
+ if (!transport && isInitializeRequest(req.body)) {
155
+ transport = new StreamableHTTPServerTransport({
156
+ sessionIdGenerator: () => randomUUID(),
157
+ onsessioninitialized: (id) => {
158
+ transports.set(id, transport);
159
+ },
160
+ });
161
+ transport.onclose = () => {
162
+ if (transport.sessionId)
163
+ transports.delete(transport.sessionId);
164
+ };
165
+ await buildServer().connect(transport);
166
+ }
167
+ if (!transport) {
168
+ res
169
+ .status(400)
170
+ .json({ jsonrpc: "2.0", error: { code: -32000, message: "No valid session" }, id: null });
171
+ return;
172
+ }
173
+ await transport.handleRequest(req, res, req.body);
174
+ });
175
+ app.get("/mcp", async (req, res) => {
176
+ const sid = req.headers["mcp-session-id"];
177
+ const t = sid ? transports.get(sid) : undefined;
178
+ if (!t) {
179
+ res.status(400).send("No session");
180
+ return;
181
+ }
182
+ await t.handleRequest(req, res);
183
+ });
184
+ app.delete("/mcp", async (req, res) => {
185
+ const sid = req.headers["mcp-session-id"];
186
+ const t = sid ? transports.get(sid) : undefined;
187
+ if (!t) {
188
+ res.status(400).send("No session");
189
+ return;
190
+ }
191
+ await t.handleRequest(req, res);
192
+ });
193
+ app.get("/health", (_req, res) => res.json({ ok: true, inbox: CHATWOOT_INBOX_ID ?? null, auth: !!AUTH_TOKEN }));
194
+ app.listen(PORT, () => {
195
+ console.log(`[mcp-chatwoot] http :${PORT} (inbox=${CHATWOOT_INBOX_ID ?? "all"}, auth=${!!AUTH_TOKEN})`);
196
+ });
197
+ }
198
+ if (TRANSPORT === "http") {
199
+ runHttp().catch((e) => {
200
+ console.error(e);
201
+ process.exit(1);
202
+ });
203
+ }
204
+ else {
205
+ runStdio().catch((e) => {
206
+ console.error(e);
207
+ process.exit(1);
208
+ });
209
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "mcp-chatwoot",
3
+ "version": "0.2.0",
4
+ "description": "MCP server read-only para Chatwoot — funciona com Claude Code, Cursor, VS Code, Antigravity, Windsurf, Cline e qualquer client MCP. Stdio + HTTP transports.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "mcp-chatwoot": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc && chmod +x dist/index.js",
17
+ "start": "node dist/index.js",
18
+ "start:http": "MCP_TRANSPORT=http node dist/index.js",
19
+ "dev": "tsx src/index.ts",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "keywords": [
23
+ "mcp",
24
+ "modelcontextprotocol",
25
+ "chatwoot",
26
+ "whatsapp",
27
+ "claude",
28
+ "cursor",
29
+ "antigravity"
30
+ ],
31
+ "author": "João Pedro Moreira <joaop13sobra@gmail.com>",
32
+ "license": "MIT",
33
+ "engines": {
34
+ "node": ">=20"
35
+ },
36
+ "dependencies": {
37
+ "@modelcontextprotocol/sdk": "^1.0.4",
38
+ "express": "^4.21.2",
39
+ "zod": "^3.23.8"
40
+ },
41
+ "devDependencies": {
42
+ "@types/express": "^5.0.0",
43
+ "@types/node": "^22.10.2",
44
+ "tsx": "^4.19.2",
45
+ "typescript": "^5.7.2"
46
+ }
47
+ }