@codespar/mcp-take-blip 0.1.0 → 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/README.md +117 -0
- package/dist/index.js +235 -8
- package/package.json +3 -2
- package/server.json +30 -0
- package/src/__tests__/index.test.ts +50 -0
- package/src/index.ts +210 -8
- package/tsconfig.json +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# @codespar/mcp-take-blip
|
|
2
|
+
|
|
3
|
+
> MCP server for **Take Blip** — chatbot and messaging platform
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@codespar/mcp-take-blip)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## Quick Start
|
|
9
|
+
|
|
10
|
+
### Claude Desktop
|
|
11
|
+
|
|
12
|
+
Add to `~/.config/claude/claude_desktop_config.json`:
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"mcpServers": {
|
|
17
|
+
"take-blip": {
|
|
18
|
+
"command": "npx",
|
|
19
|
+
"args": ["-y", "@codespar/mcp-take-blip"],
|
|
20
|
+
"env": {
|
|
21
|
+
"TAKE_BLIP_BOT_ID": "your-bot-id",
|
|
22
|
+
"TAKE_BLIP_ACCESS_KEY": "your-access-key"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Claude Code
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
claude mcp add take-blip -- npx @codespar/mcp-take-blip
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Cursor / VS Code
|
|
36
|
+
|
|
37
|
+
Add to `.cursor/mcp.json` or `.vscode/mcp.json`:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"servers": {
|
|
42
|
+
"take-blip": {
|
|
43
|
+
"command": "npx",
|
|
44
|
+
"args": ["-y", "@codespar/mcp-take-blip"],
|
|
45
|
+
"env": {
|
|
46
|
+
"TAKE_BLIP_BOT_ID": "your-bot-id",
|
|
47
|
+
"TAKE_BLIP_ACCESS_KEY": "your-access-key"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Tools
|
|
55
|
+
|
|
56
|
+
| Tool | Description |
|
|
57
|
+
|------|-------------|
|
|
58
|
+
| `send_message` | Send a message to a contact via Take Blip |
|
|
59
|
+
| `get_contacts` | List contacts in Take Blip |
|
|
60
|
+
| `create_contact` | Create a contact in Take Blip |
|
|
61
|
+
| `get_threads` | Get message threads (recent conversations) |
|
|
62
|
+
| `send_notification` | Send a notification message to a contact |
|
|
63
|
+
| `get_analytics` | Get chatbot analytics and metrics |
|
|
64
|
+
| `create_broadcast` | Create a broadcast distribution list and send messages |
|
|
65
|
+
| `get_chatbot_flow` | Get chatbot flow/builder configuration |
|
|
66
|
+
|
|
67
|
+
## Authentication
|
|
68
|
+
|
|
69
|
+
Take Blip uses a Key-based auth header computed from the bot ID and access key.
|
|
70
|
+
|
|
71
|
+
## Sandbox / Testing
|
|
72
|
+
|
|
73
|
+
Take Blip offers a free account for testing. Create a bot to get started.
|
|
74
|
+
|
|
75
|
+
### Get your credentials
|
|
76
|
+
|
|
77
|
+
1. Go to [Take Blip](https://portal.blip.ai)
|
|
78
|
+
2. Create a free account and a chatbot
|
|
79
|
+
3. Navigate to bot settings to get the bot identifier and access key
|
|
80
|
+
4. Set the environment variables
|
|
81
|
+
|
|
82
|
+
## Environment Variables
|
|
83
|
+
|
|
84
|
+
| Variable | Required | Description |
|
|
85
|
+
|----------|----------|-------------|
|
|
86
|
+
| `TAKE_BLIP_BOT_ID` | Yes | Bot identifier |
|
|
87
|
+
| `TAKE_BLIP_ACCESS_KEY` | Yes | Bot access key |
|
|
88
|
+
|
|
89
|
+
## Roadmap
|
|
90
|
+
|
|
91
|
+
### v0.2 (planned)
|
|
92
|
+
- `update_contact` — Update contact information
|
|
93
|
+
- `delete_contact` — Delete a contact
|
|
94
|
+
- `get_message_history` — Get message history for a contact
|
|
95
|
+
- `create_scheduled_message` — Schedule a message for later delivery
|
|
96
|
+
- `get_team_metrics` — Get team performance metrics
|
|
97
|
+
|
|
98
|
+
### v0.3 (planned)
|
|
99
|
+
- `flow_management` — Create and manage conversational flows
|
|
100
|
+
- `ai_model_integration` — Integrate custom AI models into flows
|
|
101
|
+
|
|
102
|
+
Want to contribute? [Open a PR](https://github.com/codespar/mcp-dev-brasil) or [request a tool](https://github.com/codespar/mcp-dev-brasil/issues).
|
|
103
|
+
|
|
104
|
+
## Links
|
|
105
|
+
|
|
106
|
+
- [Take Blip Website](https://blip.ai)
|
|
107
|
+
- [Take Blip API Documentation](https://docs.blip.ai)
|
|
108
|
+
- [MCP Dev Brasil](https://github.com/codespar/mcp-dev-brasil)
|
|
109
|
+
- [Landing Page](https://codespar.dev/mcp)
|
|
110
|
+
|
|
111
|
+
## Enterprise
|
|
112
|
+
|
|
113
|
+
Need governance, budget limits, and audit trails for agent payments? [CodeSpar Enterprise](https://codespar.dev/enterprise) adds policy engine, payment routing, and compliance templates on top of these MCP servers.
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
package/dist/index.js
CHANGED
|
@@ -2,25 +2,37 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* MCP Server for Take Blip — Brazilian chatbot and messaging platform.
|
|
4
4
|
*
|
|
5
|
-
* Tools:
|
|
5
|
+
* Tools (18):
|
|
6
6
|
* - send_message: Send a message to a contact
|
|
7
7
|
* - get_contacts: List contacts
|
|
8
8
|
* - create_contact: Create a contact
|
|
9
|
+
* - update_contact: Merge/update a contact
|
|
10
|
+
* - delete_contact: Delete a contact
|
|
11
|
+
* - get_contact: Get a single contact by identity
|
|
9
12
|
* - get_threads: Get message threads
|
|
13
|
+
* - get_thread: Get a thread between bot and an identity
|
|
10
14
|
* - send_notification: Send a notification/broadcast message
|
|
11
15
|
* - get_analytics: Get chatbot analytics
|
|
12
16
|
* - create_broadcast: Create a broadcast list and send
|
|
13
17
|
* - get_chatbot_flow: Get chatbot flow configuration
|
|
18
|
+
* - create_ticket: Open a support ticket / human handoff
|
|
19
|
+
* - close_ticket: Close an open ticket
|
|
20
|
+
* - list_tickets: List tickets in a queue
|
|
21
|
+
* - track_event: Track a custom analytics event
|
|
22
|
+
* - set_bot_resource: Set a bot resource (variable / bucket value)
|
|
23
|
+
* - get_bot_resource: Get a bot resource (variable / bucket value)
|
|
14
24
|
*
|
|
15
25
|
* Environment:
|
|
16
26
|
* TAKE_BLIP_BOT_ID — Bot identifier
|
|
17
27
|
* TAKE_BLIP_ACCESS_KEY — Bot access key
|
|
18
28
|
*
|
|
19
|
-
* Note: Take Blip uses a JSON-based messaging protocol.
|
|
29
|
+
* Note: Take Blip uses a JSON-based messaging protocol (LIME/BLiP HTTP API).
|
|
20
30
|
* Requests go as POST to /commands with type/method/uri in body.
|
|
21
31
|
*/
|
|
22
32
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
23
33
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
34
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
35
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
24
36
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
25
37
|
const BOT_ID = process.env.TAKE_BLIP_BOT_ID || "";
|
|
26
38
|
const ACCESS_KEY = process.env.TAKE_BLIP_ACCESS_KEY || "";
|
|
@@ -64,7 +76,7 @@ async function blipMessage(to, type, content) {
|
|
|
64
76
|
}
|
|
65
77
|
return res.json();
|
|
66
78
|
}
|
|
67
|
-
const server = new Server({ name: "mcp-take-blip", version: "0.
|
|
79
|
+
const server = new Server({ name: "mcp-take-blip", version: "0.2.0" }, { capabilities: { tools: {} } });
|
|
68
80
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
69
81
|
tools: [
|
|
70
82
|
{
|
|
@@ -175,6 +187,129 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
175
187
|
},
|
|
176
188
|
},
|
|
177
189
|
},
|
|
190
|
+
{
|
|
191
|
+
name: "update_contact",
|
|
192
|
+
description: "Merge/update fields on an existing contact",
|
|
193
|
+
inputSchema: {
|
|
194
|
+
type: "object",
|
|
195
|
+
properties: {
|
|
196
|
+
identity: { type: "string", description: "Contact identity (e.g., 5511999999999@wa.gw.msging.net)" },
|
|
197
|
+
name: { type: "string", description: "Contact name" },
|
|
198
|
+
email: { type: "string", description: "Contact email" },
|
|
199
|
+
phoneNumber: { type: "string", description: "Phone number" },
|
|
200
|
+
group: { type: "string", description: "Contact group" },
|
|
201
|
+
extras: { type: "object", description: "Custom extras key/value object" },
|
|
202
|
+
},
|
|
203
|
+
required: ["identity"],
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: "delete_contact",
|
|
208
|
+
description: "Delete a contact by identity",
|
|
209
|
+
inputSchema: {
|
|
210
|
+
type: "object",
|
|
211
|
+
properties: {
|
|
212
|
+
identity: { type: "string", description: "Contact identity" },
|
|
213
|
+
},
|
|
214
|
+
required: ["identity"],
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: "get_contact",
|
|
219
|
+
description: "Get a single contact by identity",
|
|
220
|
+
inputSchema: {
|
|
221
|
+
type: "object",
|
|
222
|
+
properties: {
|
|
223
|
+
identity: { type: "string", description: "Contact identity" },
|
|
224
|
+
},
|
|
225
|
+
required: ["identity"],
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: "get_thread",
|
|
230
|
+
description: "Get the message thread between the bot and a specific identity",
|
|
231
|
+
inputSchema: {
|
|
232
|
+
type: "object",
|
|
233
|
+
properties: {
|
|
234
|
+
identity: { type: "string", description: "Contact identity" },
|
|
235
|
+
take: { type: "number", description: "Number of messages to return (default 20)" },
|
|
236
|
+
},
|
|
237
|
+
required: ["identity"],
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: "create_ticket",
|
|
242
|
+
description: "Open a support ticket / human handoff for a contact",
|
|
243
|
+
inputSchema: {
|
|
244
|
+
type: "object",
|
|
245
|
+
properties: {
|
|
246
|
+
customerIdentity: { type: "string", description: "Contact identity to open ticket for" },
|
|
247
|
+
team: { type: "string", description: "Agent team / queue name" },
|
|
248
|
+
},
|
|
249
|
+
required: ["customerIdentity"],
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: "close_ticket",
|
|
254
|
+
description: "Close an open support ticket",
|
|
255
|
+
inputSchema: {
|
|
256
|
+
type: "object",
|
|
257
|
+
properties: {
|
|
258
|
+
ticketId: { type: "string", description: "Ticket id to close" },
|
|
259
|
+
},
|
|
260
|
+
required: ["ticketId"],
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: "list_tickets",
|
|
265
|
+
description: "List tickets, optionally filtering by status",
|
|
266
|
+
inputSchema: {
|
|
267
|
+
type: "object",
|
|
268
|
+
properties: {
|
|
269
|
+
status: { type: "string", description: "Ticket status filter (e.g., Open, Waiting, Closed)" },
|
|
270
|
+
skip: { type: "number", description: "Pagination skip" },
|
|
271
|
+
take: { type: "number", description: "Pagination take (default 20)" },
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
name: "track_event",
|
|
277
|
+
description: "Track a custom analytics event in the bot event tracker",
|
|
278
|
+
inputSchema: {
|
|
279
|
+
type: "object",
|
|
280
|
+
properties: {
|
|
281
|
+
category: { type: "string", description: "Event category" },
|
|
282
|
+
action: { type: "string", description: "Event action" },
|
|
283
|
+
extras: { type: "object", description: "Additional event metadata" },
|
|
284
|
+
contactIdentity: { type: "string", description: "Contact identity associated with the event" },
|
|
285
|
+
},
|
|
286
|
+
required: ["category", "action"],
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
name: "set_bot_resource",
|
|
291
|
+
description: "Set a bot resource value (used as bot variables / state via /resources bucket)",
|
|
292
|
+
inputSchema: {
|
|
293
|
+
type: "object",
|
|
294
|
+
properties: {
|
|
295
|
+
name: { type: "string", description: "Resource name (key)" },
|
|
296
|
+
value: { description: "Resource value (string, number, object)" },
|
|
297
|
+
type: { type: "string", description: "MIME type (default text/plain; use application/json for objects)" },
|
|
298
|
+
},
|
|
299
|
+
required: ["name", "value"],
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
name: "get_bot_resource",
|
|
304
|
+
description: "Get a bot resource value by name (variable / state)",
|
|
305
|
+
inputSchema: {
|
|
306
|
+
type: "object",
|
|
307
|
+
properties: {
|
|
308
|
+
name: { type: "string", description: "Resource name (key)" },
|
|
309
|
+
},
|
|
310
|
+
required: ["name"],
|
|
311
|
+
},
|
|
312
|
+
},
|
|
178
313
|
],
|
|
179
314
|
}));
|
|
180
315
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -237,6 +372,63 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
237
372
|
const uri = args?.flowId ? `/buckets/blip_portal:builder_working_flow_${args.flowId}` : "/buckets/blip_portal:builder_working_flow";
|
|
238
373
|
return { content: [{ type: "text", text: JSON.stringify(await blipCommand(crypto.randomUUID(), "get", uri), null, 2) }] };
|
|
239
374
|
}
|
|
375
|
+
case "update_contact": {
|
|
376
|
+
const resource = { identity: args?.identity };
|
|
377
|
+
if (args?.name)
|
|
378
|
+
resource.name = args.name;
|
|
379
|
+
if (args?.email)
|
|
380
|
+
resource.email = args.email;
|
|
381
|
+
if (args?.phoneNumber)
|
|
382
|
+
resource.phoneNumber = args.phoneNumber;
|
|
383
|
+
if (args?.group)
|
|
384
|
+
resource.group = args.group;
|
|
385
|
+
if (args?.extras)
|
|
386
|
+
resource.extras = args.extras;
|
|
387
|
+
return { content: [{ type: "text", text: JSON.stringify(await blipCommand(crypto.randomUUID(), "merge", "/contacts", "application/vnd.lime.contact+json", resource), null, 2) }] };
|
|
388
|
+
}
|
|
389
|
+
case "delete_contact": {
|
|
390
|
+
return { content: [{ type: "text", text: JSON.stringify(await blipCommand(crypto.randomUUID(), "delete", `/contacts/${encodeURIComponent(String(args?.identity))}`), null, 2) }] };
|
|
391
|
+
}
|
|
392
|
+
case "get_contact": {
|
|
393
|
+
return { content: [{ type: "text", text: JSON.stringify(await blipCommand(crypto.randomUUID(), "get", `/contacts/${encodeURIComponent(String(args?.identity))}`), null, 2) }] };
|
|
394
|
+
}
|
|
395
|
+
case "get_thread": {
|
|
396
|
+
const take = args?.take ? `$take=${args.take}` : "$take=20";
|
|
397
|
+
return { content: [{ type: "text", text: JSON.stringify(await blipCommand(crypto.randomUUID(), "get", `/threads/${encodeURIComponent(String(args?.identity))}?${take}`), null, 2) }] };
|
|
398
|
+
}
|
|
399
|
+
case "create_ticket": {
|
|
400
|
+
const resource = { customerIdentity: args?.customerIdentity };
|
|
401
|
+
if (args?.team)
|
|
402
|
+
resource.team = args.team;
|
|
403
|
+
return { content: [{ type: "text", text: JSON.stringify(await blipCommand(crypto.randomUUID(), "set", "/tickets", "application/vnd.iris.ticket+json", resource), null, 2) }] };
|
|
404
|
+
}
|
|
405
|
+
case "close_ticket": {
|
|
406
|
+
return { content: [{ type: "text", text: JSON.stringify(await blipCommand(crypto.randomUUID(), "set", `/tickets/${encodeURIComponent(String(args?.ticketId))}/change-status`, "application/vnd.iris.ticket+json", { status: "ClosedAttendant" }), null, 2) }] };
|
|
407
|
+
}
|
|
408
|
+
case "list_tickets": {
|
|
409
|
+
const filters = [];
|
|
410
|
+
if (args?.status)
|
|
411
|
+
filters.push(`$filter=${encodeURIComponent(`status eq '${args.status}'`)}`);
|
|
412
|
+
if (args?.skip)
|
|
413
|
+
filters.push(`$skip=${args.skip}`);
|
|
414
|
+
filters.push(args?.take ? `$take=${args.take}` : "$take=20");
|
|
415
|
+
return { content: [{ type: "text", text: JSON.stringify(await blipCommand(crypto.randomUUID(), "get", `/tickets?${filters.join("&")}`), null, 2) }] };
|
|
416
|
+
}
|
|
417
|
+
case "track_event": {
|
|
418
|
+
const resource = { category: args?.category, action: args?.action };
|
|
419
|
+
if (args?.extras)
|
|
420
|
+
resource.extras = args.extras;
|
|
421
|
+
if (args?.contactIdentity)
|
|
422
|
+
resource.contactIdentity = args.contactIdentity;
|
|
423
|
+
return { content: [{ type: "text", text: JSON.stringify(await blipCommand(crypto.randomUUID(), "set", "/event-track", "application/vnd.iris.eventTrack+json", resource), null, 2) }] };
|
|
424
|
+
}
|
|
425
|
+
case "set_bot_resource": {
|
|
426
|
+
const type = String(args?.type || (typeof args?.value === "object" ? "application/json" : "text/plain"));
|
|
427
|
+
return { content: [{ type: "text", text: JSON.stringify(await blipCommand(crypto.randomUUID(), "set", `/resources/${encodeURIComponent(String(args?.name))}`, type, args?.value), null, 2) }] };
|
|
428
|
+
}
|
|
429
|
+
case "get_bot_resource": {
|
|
430
|
+
return { content: [{ type: "text", text: JSON.stringify(await blipCommand(crypto.randomUUID(), "get", `/resources/${encodeURIComponent(String(args?.name))}`), null, 2) }] };
|
|
431
|
+
}
|
|
240
432
|
default:
|
|
241
433
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
242
434
|
}
|
|
@@ -246,11 +438,46 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
246
438
|
}
|
|
247
439
|
});
|
|
248
440
|
async function main() {
|
|
249
|
-
if (
|
|
250
|
-
|
|
251
|
-
|
|
441
|
+
if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
|
|
442
|
+
const { default: express } = await import("express");
|
|
443
|
+
const { randomUUID } = await import("node:crypto");
|
|
444
|
+
const app = express();
|
|
445
|
+
app.use(express.json());
|
|
446
|
+
const transports = new Map();
|
|
447
|
+
app.get("/health", (_req, res) => res.json({ status: "ok", sessions: transports.size }));
|
|
448
|
+
app.post("/mcp", async (req, res) => {
|
|
449
|
+
const sid = req.headers["mcp-session-id"];
|
|
450
|
+
if (sid && transports.has(sid)) {
|
|
451
|
+
await transports.get(sid).handleRequest(req, res, req.body);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (!sid && isInitializeRequest(req.body)) {
|
|
455
|
+
const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
|
|
456
|
+
t.onclose = () => { if (t.sessionId)
|
|
457
|
+
transports.delete(t.sessionId); };
|
|
458
|
+
const s = new Server({ name: "mcp-take-blip", version: "0.2.0" }, { capabilities: { tools: {} } });
|
|
459
|
+
server._requestHandlers.forEach((v, k) => s._requestHandlers.set(k, v));
|
|
460
|
+
server._notificationHandlers?.forEach((v, k) => s._notificationHandlers.set(k, v));
|
|
461
|
+
await s.connect(t);
|
|
462
|
+
await t.handleRequest(req, res, req.body);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
|
|
466
|
+
});
|
|
467
|
+
app.get("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
|
|
468
|
+
await transports.get(sid).handleRequest(req, res);
|
|
469
|
+
else
|
|
470
|
+
res.status(400).send("Invalid session"); });
|
|
471
|
+
app.delete("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
|
|
472
|
+
await transports.get(sid).handleRequest(req, res);
|
|
473
|
+
else
|
|
474
|
+
res.status(400).send("Invalid session"); });
|
|
475
|
+
const port = Number(process.env.MCP_PORT) || 3000;
|
|
476
|
+
app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
const transport = new StdioServerTransport();
|
|
480
|
+
await server.connect(transport);
|
|
252
481
|
}
|
|
253
|
-
const transport = new StdioServerTransport();
|
|
254
|
-
await server.connect(transport);
|
|
255
482
|
}
|
|
256
483
|
main().catch(console.error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codespar/mcp-take-blip",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "MCP server for Take Blip — chatbots, messaging, contacts, broadcasts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -26,5 +26,6 @@
|
|
|
26
26
|
"messaging",
|
|
27
27
|
"whatsapp",
|
|
28
28
|
"brazil"
|
|
29
|
-
]
|
|
29
|
+
],
|
|
30
|
+
"mcpName": "io.github.codespar/mcp-take-blip"
|
|
30
31
|
}
|
package/server.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.codespar/mcp-take-blip",
|
|
4
|
+
"description": "MCP server for Take Blip — chatbots, messaging, contacts, broadcasts",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "https://github.com/codespar/mcp-dev-brasil",
|
|
7
|
+
"source": "github",
|
|
8
|
+
"subfolder": "packages/communication/take-blip"
|
|
9
|
+
},
|
|
10
|
+
"version": "0.2.0",
|
|
11
|
+
"packages": [
|
|
12
|
+
{
|
|
13
|
+
"registryType": "npm",
|
|
14
|
+
"identifier": "@codespar/mcp-take-blip",
|
|
15
|
+
"version": "0.2.0",
|
|
16
|
+
"transport": {
|
|
17
|
+
"type": "stdio"
|
|
18
|
+
},
|
|
19
|
+
"environmentVariables": [
|
|
20
|
+
{
|
|
21
|
+
"name": "TAKE_BLIP_API_KEY",
|
|
22
|
+
"description": "API key for take-blip",
|
|
23
|
+
"isRequired": true,
|
|
24
|
+
"format": "string",
|
|
25
|
+
"isSecret": true
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
let listToolsHandler: Function;
|
|
4
|
+
let callToolHandler: Function;
|
|
5
|
+
|
|
6
|
+
vi.mock("@modelcontextprotocol/sdk/server/index.js", () => {
|
|
7
|
+
class FakeServer {
|
|
8
|
+
constructor() {}
|
|
9
|
+
setRequestHandler(schema: any, handler: Function) {
|
|
10
|
+
if (JSON.stringify(schema).includes("tools/list")) listToolsHandler = handler;
|
|
11
|
+
if (JSON.stringify(schema).includes("tools/call")) callToolHandler = handler;
|
|
12
|
+
}
|
|
13
|
+
connect() { return Promise.resolve(); }
|
|
14
|
+
}
|
|
15
|
+
return { Server: FakeServer };
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
vi.mock("@modelcontextprotocol/sdk/server/stdio.js", () => ({ StdioServerTransport: class {} }));
|
|
19
|
+
|
|
20
|
+
process.env.TAKE_BLIP_BOT_ID = "testbot";
|
|
21
|
+
process.env.TAKE_BLIP_ACCESS_KEY = "test-key";
|
|
22
|
+
|
|
23
|
+
const mockFetch = vi.fn();
|
|
24
|
+
global.fetch = mockFetch as any;
|
|
25
|
+
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
vi.resetModules();
|
|
28
|
+
listToolsHandler = undefined as any;
|
|
29
|
+
callToolHandler = undefined as any;
|
|
30
|
+
mockFetch.mockReset();
|
|
31
|
+
global.fetch = mockFetch as any;
|
|
32
|
+
await import("../index.js");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("mcp-take-blip", () => {
|
|
36
|
+
it("should register 8 tools", async () => {
|
|
37
|
+
const result = await listToolsHandler();
|
|
38
|
+
expect(result.tools).toHaveLength(8);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should call correct API endpoint for get_contacts", async () => {
|
|
42
|
+
mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ status: "success", resource: { items: [] } }) });
|
|
43
|
+
|
|
44
|
+
await callToolHandler({ params: { name: "get_contacts", arguments: {} } });
|
|
45
|
+
|
|
46
|
+
const [url, opts] = mockFetch.mock.calls[0];
|
|
47
|
+
expect(url).toContain("msging.net/commands");
|
|
48
|
+
expect(opts.method).toBe("POST");
|
|
49
|
+
});
|
|
50
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -3,26 +3,38 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* MCP Server for Take Blip — Brazilian chatbot and messaging platform.
|
|
5
5
|
*
|
|
6
|
-
* Tools:
|
|
6
|
+
* Tools (18):
|
|
7
7
|
* - send_message: Send a message to a contact
|
|
8
8
|
* - get_contacts: List contacts
|
|
9
9
|
* - create_contact: Create a contact
|
|
10
|
+
* - update_contact: Merge/update a contact
|
|
11
|
+
* - delete_contact: Delete a contact
|
|
12
|
+
* - get_contact: Get a single contact by identity
|
|
10
13
|
* - get_threads: Get message threads
|
|
14
|
+
* - get_thread: Get a thread between bot and an identity
|
|
11
15
|
* - send_notification: Send a notification/broadcast message
|
|
12
16
|
* - get_analytics: Get chatbot analytics
|
|
13
17
|
* - create_broadcast: Create a broadcast list and send
|
|
14
18
|
* - get_chatbot_flow: Get chatbot flow configuration
|
|
19
|
+
* - create_ticket: Open a support ticket / human handoff
|
|
20
|
+
* - close_ticket: Close an open ticket
|
|
21
|
+
* - list_tickets: List tickets in a queue
|
|
22
|
+
* - track_event: Track a custom analytics event
|
|
23
|
+
* - set_bot_resource: Set a bot resource (variable / bucket value)
|
|
24
|
+
* - get_bot_resource: Get a bot resource (variable / bucket value)
|
|
15
25
|
*
|
|
16
26
|
* Environment:
|
|
17
27
|
* TAKE_BLIP_BOT_ID — Bot identifier
|
|
18
28
|
* TAKE_BLIP_ACCESS_KEY — Bot access key
|
|
19
29
|
*
|
|
20
|
-
* Note: Take Blip uses a JSON-based messaging protocol.
|
|
30
|
+
* Note: Take Blip uses a JSON-based messaging protocol (LIME/BLiP HTTP API).
|
|
21
31
|
* Requests go as POST to /commands with type/method/uri in body.
|
|
22
32
|
*/
|
|
23
33
|
|
|
24
34
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
25
35
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
36
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
37
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
26
38
|
import {
|
|
27
39
|
CallToolRequestSchema,
|
|
28
40
|
ListToolsRequestSchema,
|
|
@@ -74,7 +86,7 @@ async function blipMessage(to: string, type: string, content: unknown): Promise<
|
|
|
74
86
|
}
|
|
75
87
|
|
|
76
88
|
const server = new Server(
|
|
77
|
-
{ name: "mcp-take-blip", version: "0.
|
|
89
|
+
{ name: "mcp-take-blip", version: "0.2.0" },
|
|
78
90
|
{ capabilities: { tools: {} } }
|
|
79
91
|
);
|
|
80
92
|
|
|
@@ -188,6 +200,129 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
188
200
|
},
|
|
189
201
|
},
|
|
190
202
|
},
|
|
203
|
+
{
|
|
204
|
+
name: "update_contact",
|
|
205
|
+
description: "Merge/update fields on an existing contact",
|
|
206
|
+
inputSchema: {
|
|
207
|
+
type: "object",
|
|
208
|
+
properties: {
|
|
209
|
+
identity: { type: "string", description: "Contact identity (e.g., 5511999999999@wa.gw.msging.net)" },
|
|
210
|
+
name: { type: "string", description: "Contact name" },
|
|
211
|
+
email: { type: "string", description: "Contact email" },
|
|
212
|
+
phoneNumber: { type: "string", description: "Phone number" },
|
|
213
|
+
group: { type: "string", description: "Contact group" },
|
|
214
|
+
extras: { type: "object", description: "Custom extras key/value object" },
|
|
215
|
+
},
|
|
216
|
+
required: ["identity"],
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: "delete_contact",
|
|
221
|
+
description: "Delete a contact by identity",
|
|
222
|
+
inputSchema: {
|
|
223
|
+
type: "object",
|
|
224
|
+
properties: {
|
|
225
|
+
identity: { type: "string", description: "Contact identity" },
|
|
226
|
+
},
|
|
227
|
+
required: ["identity"],
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: "get_contact",
|
|
232
|
+
description: "Get a single contact by identity",
|
|
233
|
+
inputSchema: {
|
|
234
|
+
type: "object",
|
|
235
|
+
properties: {
|
|
236
|
+
identity: { type: "string", description: "Contact identity" },
|
|
237
|
+
},
|
|
238
|
+
required: ["identity"],
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
name: "get_thread",
|
|
243
|
+
description: "Get the message thread between the bot and a specific identity",
|
|
244
|
+
inputSchema: {
|
|
245
|
+
type: "object",
|
|
246
|
+
properties: {
|
|
247
|
+
identity: { type: "string", description: "Contact identity" },
|
|
248
|
+
take: { type: "number", description: "Number of messages to return (default 20)" },
|
|
249
|
+
},
|
|
250
|
+
required: ["identity"],
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: "create_ticket",
|
|
255
|
+
description: "Open a support ticket / human handoff for a contact",
|
|
256
|
+
inputSchema: {
|
|
257
|
+
type: "object",
|
|
258
|
+
properties: {
|
|
259
|
+
customerIdentity: { type: "string", description: "Contact identity to open ticket for" },
|
|
260
|
+
team: { type: "string", description: "Agent team / queue name" },
|
|
261
|
+
},
|
|
262
|
+
required: ["customerIdentity"],
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: "close_ticket",
|
|
267
|
+
description: "Close an open support ticket",
|
|
268
|
+
inputSchema: {
|
|
269
|
+
type: "object",
|
|
270
|
+
properties: {
|
|
271
|
+
ticketId: { type: "string", description: "Ticket id to close" },
|
|
272
|
+
},
|
|
273
|
+
required: ["ticketId"],
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: "list_tickets",
|
|
278
|
+
description: "List tickets, optionally filtering by status",
|
|
279
|
+
inputSchema: {
|
|
280
|
+
type: "object",
|
|
281
|
+
properties: {
|
|
282
|
+
status: { type: "string", description: "Ticket status filter (e.g., Open, Waiting, Closed)" },
|
|
283
|
+
skip: { type: "number", description: "Pagination skip" },
|
|
284
|
+
take: { type: "number", description: "Pagination take (default 20)" },
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: "track_event",
|
|
290
|
+
description: "Track a custom analytics event in the bot event tracker",
|
|
291
|
+
inputSchema: {
|
|
292
|
+
type: "object",
|
|
293
|
+
properties: {
|
|
294
|
+
category: { type: "string", description: "Event category" },
|
|
295
|
+
action: { type: "string", description: "Event action" },
|
|
296
|
+
extras: { type: "object", description: "Additional event metadata" },
|
|
297
|
+
contactIdentity: { type: "string", description: "Contact identity associated with the event" },
|
|
298
|
+
},
|
|
299
|
+
required: ["category", "action"],
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
name: "set_bot_resource",
|
|
304
|
+
description: "Set a bot resource value (used as bot variables / state via /resources bucket)",
|
|
305
|
+
inputSchema: {
|
|
306
|
+
type: "object",
|
|
307
|
+
properties: {
|
|
308
|
+
name: { type: "string", description: "Resource name (key)" },
|
|
309
|
+
value: { description: "Resource value (string, number, object)" },
|
|
310
|
+
type: { type: "string", description: "MIME type (default text/plain; use application/json for objects)" },
|
|
311
|
+
},
|
|
312
|
+
required: ["name", "value"],
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: "get_bot_resource",
|
|
317
|
+
description: "Get a bot resource value by name (variable / state)",
|
|
318
|
+
inputSchema: {
|
|
319
|
+
type: "object",
|
|
320
|
+
properties: {
|
|
321
|
+
name: { type: "string", description: "Resource name (key)" },
|
|
322
|
+
},
|
|
323
|
+
required: ["name"],
|
|
324
|
+
},
|
|
325
|
+
},
|
|
191
326
|
],
|
|
192
327
|
}));
|
|
193
328
|
|
|
@@ -252,6 +387,53 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
252
387
|
const uri = args?.flowId ? `/buckets/blip_portal:builder_working_flow_${args.flowId}` : "/buckets/blip_portal:builder_working_flow";
|
|
253
388
|
return { content: [{ type: "text", text: JSON.stringify(await blipCommand(crypto.randomUUID(), "get", uri), null, 2) }] };
|
|
254
389
|
}
|
|
390
|
+
case "update_contact": {
|
|
391
|
+
const resource: Record<string, unknown> = { identity: args?.identity };
|
|
392
|
+
if (args?.name) resource.name = args.name;
|
|
393
|
+
if (args?.email) resource.email = args.email;
|
|
394
|
+
if (args?.phoneNumber) resource.phoneNumber = args.phoneNumber;
|
|
395
|
+
if (args?.group) resource.group = args.group;
|
|
396
|
+
if (args?.extras) resource.extras = args.extras;
|
|
397
|
+
return { content: [{ type: "text", text: JSON.stringify(await blipCommand(crypto.randomUUID(), "merge", "/contacts", "application/vnd.lime.contact+json", resource), null, 2) }] };
|
|
398
|
+
}
|
|
399
|
+
case "delete_contact": {
|
|
400
|
+
return { content: [{ type: "text", text: JSON.stringify(await blipCommand(crypto.randomUUID(), "delete", `/contacts/${encodeURIComponent(String(args?.identity))}`), null, 2) }] };
|
|
401
|
+
}
|
|
402
|
+
case "get_contact": {
|
|
403
|
+
return { content: [{ type: "text", text: JSON.stringify(await blipCommand(crypto.randomUUID(), "get", `/contacts/${encodeURIComponent(String(args?.identity))}`), null, 2) }] };
|
|
404
|
+
}
|
|
405
|
+
case "get_thread": {
|
|
406
|
+
const take = args?.take ? `$take=${args.take}` : "$take=20";
|
|
407
|
+
return { content: [{ type: "text", text: JSON.stringify(await blipCommand(crypto.randomUUID(), "get", `/threads/${encodeURIComponent(String(args?.identity))}?${take}`), null, 2) }] };
|
|
408
|
+
}
|
|
409
|
+
case "create_ticket": {
|
|
410
|
+
const resource: Record<string, unknown> = { customerIdentity: args?.customerIdentity };
|
|
411
|
+
if (args?.team) resource.team = args.team;
|
|
412
|
+
return { content: [{ type: "text", text: JSON.stringify(await blipCommand(crypto.randomUUID(), "set", "/tickets", "application/vnd.iris.ticket+json", resource), null, 2) }] };
|
|
413
|
+
}
|
|
414
|
+
case "close_ticket": {
|
|
415
|
+
return { content: [{ type: "text", text: JSON.stringify(await blipCommand(crypto.randomUUID(), "set", `/tickets/${encodeURIComponent(String(args?.ticketId))}/change-status`, "application/vnd.iris.ticket+json", { status: "ClosedAttendant" }), null, 2) }] };
|
|
416
|
+
}
|
|
417
|
+
case "list_tickets": {
|
|
418
|
+
const filters: string[] = [];
|
|
419
|
+
if (args?.status) filters.push(`$filter=${encodeURIComponent(`status eq '${args.status}'`)}`);
|
|
420
|
+
if (args?.skip) filters.push(`$skip=${args.skip}`);
|
|
421
|
+
filters.push(args?.take ? `$take=${args.take}` : "$take=20");
|
|
422
|
+
return { content: [{ type: "text", text: JSON.stringify(await blipCommand(crypto.randomUUID(), "get", `/tickets?${filters.join("&")}`), null, 2) }] };
|
|
423
|
+
}
|
|
424
|
+
case "track_event": {
|
|
425
|
+
const resource: Record<string, unknown> = { category: args?.category, action: args?.action };
|
|
426
|
+
if (args?.extras) resource.extras = args.extras;
|
|
427
|
+
if (args?.contactIdentity) resource.contactIdentity = args.contactIdentity;
|
|
428
|
+
return { content: [{ type: "text", text: JSON.stringify(await blipCommand(crypto.randomUUID(), "set", "/event-track", "application/vnd.iris.eventTrack+json", resource), null, 2) }] };
|
|
429
|
+
}
|
|
430
|
+
case "set_bot_resource": {
|
|
431
|
+
const type = String(args?.type || (typeof args?.value === "object" ? "application/json" : "text/plain"));
|
|
432
|
+
return { content: [{ type: "text", text: JSON.stringify(await blipCommand(crypto.randomUUID(), "set", `/resources/${encodeURIComponent(String(args?.name))}`, type, args?.value), null, 2) }] };
|
|
433
|
+
}
|
|
434
|
+
case "get_bot_resource": {
|
|
435
|
+
return { content: [{ type: "text", text: JSON.stringify(await blipCommand(crypto.randomUUID(), "get", `/resources/${encodeURIComponent(String(args?.name))}`), null, 2) }] };
|
|
436
|
+
}
|
|
255
437
|
default:
|
|
256
438
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
257
439
|
}
|
|
@@ -261,12 +443,32 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
261
443
|
});
|
|
262
444
|
|
|
263
445
|
async function main() {
|
|
264
|
-
if (
|
|
265
|
-
|
|
266
|
-
|
|
446
|
+
if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
|
|
447
|
+
const { default: express } = await import("express");
|
|
448
|
+
const { randomUUID } = await import("node:crypto");
|
|
449
|
+
const app = express();
|
|
450
|
+
app.use(express.json());
|
|
451
|
+
const transports = new Map<string, StreamableHTTPServerTransport>();
|
|
452
|
+
app.get("/health", (_req: any, res: any) => res.json({ status: "ok", sessions: transports.size }));
|
|
453
|
+
app.post("/mcp", async (req: any, res: any) => {
|
|
454
|
+
const sid = req.headers["mcp-session-id"] as string | undefined;
|
|
455
|
+
if (sid && transports.has(sid)) { await transports.get(sid)!.handleRequest(req, res, req.body); return; }
|
|
456
|
+
if (!sid && isInitializeRequest(req.body)) {
|
|
457
|
+
const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
|
|
458
|
+
t.onclose = () => { if (t.sessionId) transports.delete(t.sessionId); };
|
|
459
|
+
const s = new Server({ name: "mcp-take-blip", version: "0.2.0" }, { capabilities: { tools: {} } }); (server as any)._requestHandlers.forEach((v: any, k: any) => (s as any)._requestHandlers.set(k, v)); (server as any)._notificationHandlers?.forEach((v: any, k: any) => (s as any)._notificationHandlers.set(k, v)); await s.connect(t);
|
|
460
|
+
await t.handleRequest(req, res, req.body); return;
|
|
461
|
+
}
|
|
462
|
+
res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
|
|
463
|
+
});
|
|
464
|
+
app.get("/mcp", async (req: any, res: any) => { const sid = req.headers["mcp-session-id"] as string; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req, res); else res.status(400).send("Invalid session"); });
|
|
465
|
+
app.delete("/mcp", async (req: any, res: any) => { const sid = req.headers["mcp-session-id"] as string; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req, res); else res.status(400).send("Invalid session"); });
|
|
466
|
+
const port = Number(process.env.MCP_PORT) || 3000;
|
|
467
|
+
app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
|
|
468
|
+
} else {
|
|
469
|
+
const transport = new StdioServerTransport();
|
|
470
|
+
await server.connect(transport);
|
|
267
471
|
}
|
|
268
|
-
const transport = new StdioServerTransport();
|
|
269
|
-
await server.connect(transport);
|
|
270
472
|
}
|
|
271
473
|
|
|
272
474
|
main().catch(console.error);
|