@codespar/mcp-whatsapp-cloud 0.1.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 ADDED
@@ -0,0 +1,103 @@
1
+ # @codespar/mcp-whatsapp-cloud
2
+
3
+ MCP server for the [WhatsApp Cloud API](https://developers.facebook.com/docs/whatsapp/cloud-api) — Meta's official WhatsApp Business API, self-hosted on Meta infrastructure.
4
+
5
+ **Direct Meta integration.** No middleman, no provider markup. For merchants with an approved WhatsApp Business Account (WABA) who want Meta-direct pricing and full control over conversation, pricing category, and template lifecycle.
6
+
7
+ ## WhatsApp servers in this catalog
8
+
9
+ | Server | What it is | When to pick it |
10
+ |--------|------------|-----------------|
11
+ | **whatsapp-cloud** (this) | Direct Meta Cloud API | Large merchants with approved WABA; lower cost at scale, no intermediary fees |
12
+ | z-api | Wrapper on top of Meta Cloud | Easy onboarding, instant QR-pair, extra helpers |
13
+ | evolution-api | Open-source wrapper | Self-hosted, community-driven |
14
+ | take-blip | Brazilian BSP wrapper | Enterprise Brazil, CCaaS features |
15
+ | zenvia | Brazilian BSP wrapper | Brazil omnichannel (SMS + WhatsApp) |
16
+
17
+ ## Tools
18
+
19
+ | Tool | Purpose |
20
+ |------|---------|
21
+ | `send_text_message` | Send a plain text message |
22
+ | `send_template_message` | Send an approved template (required for business-initiated > 24h) |
23
+ | `send_media_message` | Send image, video, document, or audio (link or media_id) |
24
+ | `send_interactive_message` | Send reply buttons or list |
25
+ | `send_location_message` | Send a latitude/longitude pin |
26
+ | `mark_message_as_read` | Mark an inbound message as read |
27
+ | `upload_media` | Upload a file and get a reusable media_id (multipart) |
28
+ | `retrieve_media_url` | Resolve a media_id to a downloadable URL |
29
+ | `delete_media` | Delete an uploaded media asset |
30
+ | `list_templates` | List templates on the WABA |
31
+ | `create_template` | Submit a new template for Meta review |
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ npm install @codespar/mcp-whatsapp-cloud
37
+ ```
38
+
39
+ ## Environment
40
+
41
+ ```bash
42
+ WHATSAPP_ACCESS_TOKEN="EAAG..." # required (secret) — Meta system-user token
43
+ WHATSAPP_PHONE_NUMBER_ID="1234567890" # required — WABA phone number id
44
+ WHATSAPP_BUSINESS_ACCOUNT_ID="9876543210" # required — WABA id (for templates)
45
+ WHATSAPP_GRAPH_VERSION="v21.0" # optional — Meta bumps quarterly
46
+ ```
47
+
48
+ ## Authentication
49
+
50
+ Bearer token against the Graph API. Use a **permanent system-user token** from Meta Business Manager — user access tokens expire and will break production.
51
+
52
+ ```
53
+ Authorization: Bearer <WHATSAPP_ACCESS_TOKEN>
54
+ ```
55
+
56
+ ## Messaging rules (important)
57
+
58
+ - **Customer-service window** — You can freely send any message type for 24h after the user last messaged you.
59
+ - **Business-initiated** — Outside that window you must send an approved **template**. Use `send_template_message` with a name + language + components.
60
+ - **Templates** — Create with `create_template`, wait for Meta approval (minutes to hours), then use.
61
+ - Phone numbers are **E.164 without the leading `+`** (e.g. `5511999999999`).
62
+
63
+ ## Media
64
+
65
+ Two paths:
66
+
67
+ 1. **Public URL** — pass `link` to `send_media_message`. Fastest, but Meta fetches on every send.
68
+ 2. **Uploaded media_id** — call `upload_media` once, reuse `id` on subsequent sends. Recommended for catalog assets.
69
+
70
+ Uploaded media expires after ~30 days.
71
+
72
+ ## Interactive messages
73
+
74
+ `send_interactive_message` expects a fully-formed `interactive` object. Example button payload:
75
+
76
+ ```json
77
+ {
78
+ "type": "button",
79
+ "body": { "text": "Confirma seu pedido?" },
80
+ "action": {
81
+ "buttons": [
82
+ { "type": "reply", "reply": { "id": "confirm", "title": "Confirmar" } },
83
+ { "type": "reply", "reply": { "id": "cancel", "title": "Cancelar" } }
84
+ ]
85
+ }
86
+ }
87
+ ```
88
+
89
+ See the [Cloud API interactive reference](https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-message-templates) for list payloads.
90
+
91
+ ## Run
92
+
93
+ ```bash
94
+ # stdio (default — for Claude Desktop, Cursor, etc)
95
+ npx @codespar/mcp-whatsapp-cloud
96
+
97
+ # HTTP (for server-to-server)
98
+ MCP_HTTP=true MCP_PORT=3000 npx @codespar/mcp-whatsapp-cloud
99
+ ```
100
+
101
+ ## License
102
+
103
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,427 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP Server for WhatsApp Cloud API — Meta's official Business API.
4
+ *
5
+ * Direct integration with Meta Graph API. No middleman. Distinct from the
6
+ * wrapper providers in the catalog (Z-API, Evolution, Take Blip, Zenvia) which
7
+ * all sit on top of this. Target: merchants with an approved WhatsApp Business
8
+ * Account (WABA) who want Meta-direct pricing and full control.
9
+ *
10
+ * Tools (11):
11
+ * send_text_message — simple text message
12
+ * send_template_message — approved template (required for 24h+ business-initiated)
13
+ * send_media_message — image, video, document, or audio
14
+ * send_interactive_message — buttons or list
15
+ * send_location_message — latitude/longitude pin
16
+ * mark_message_as_read — mark an incoming message as read
17
+ * upload_media — upload a file and get a media_id (multipart)
18
+ * retrieve_media_url — resolve a media_id to a downloadable URL
19
+ * delete_media — delete an uploaded media asset
20
+ * list_templates — list templates on the WABA
21
+ * create_template — submit a new template for Meta review
22
+ *
23
+ * Authentication
24
+ * Bearer token (permanent system-user access token).
25
+ * Authorization: Bearer <WHATSAPP_ACCESS_TOKEN>
26
+ *
27
+ * API surface
28
+ * Graph API base: https://graph.facebook.com/{version}
29
+ * Message + media endpoints are scoped to a phone_number_id.
30
+ * Template endpoints are scoped to a business_account_id.
31
+ *
32
+ * Environment
33
+ * WHATSAPP_ACCESS_TOKEN required — Meta system-user token (secret)
34
+ * WHATSAPP_PHONE_NUMBER_ID required — WABA phone number id
35
+ * WHATSAPP_BUSINESS_ACCOUNT_ID required — WABA id (for template management)
36
+ * WHATSAPP_GRAPH_VERSION optional — default v21.0
37
+ *
38
+ * Docs: https://developers.facebook.com/docs/whatsapp/cloud-api
39
+ */
40
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
41
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
42
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
43
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
44
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
45
+ const ACCESS_TOKEN = process.env.WHATSAPP_ACCESS_TOKEN || "";
46
+ const PHONE_NUMBER_ID = process.env.WHATSAPP_PHONE_NUMBER_ID || "";
47
+ const BUSINESS_ACCOUNT_ID = process.env.WHATSAPP_BUSINESS_ACCOUNT_ID || "";
48
+ const GRAPH_VERSION = process.env.WHATSAPP_GRAPH_VERSION || "v21.0";
49
+ const BASE_URL = `https://graph.facebook.com/${GRAPH_VERSION}`;
50
+ async function whatsappRequest(method, path, body, opts = {}) {
51
+ const url = `${BASE_URL}${path}`;
52
+ const headers = {
53
+ Authorization: `Bearer ${ACCESS_TOKEN}`,
54
+ Accept: "application/json",
55
+ };
56
+ let payload;
57
+ if (body !== undefined && body !== null) {
58
+ if (opts.multipart) {
59
+ const form = new FormData();
60
+ const b = body;
61
+ for (const [k, v] of Object.entries(b)) {
62
+ if (v === undefined || v === null)
63
+ continue;
64
+ if (k === "file" && typeof v === "object" && v !== null && "data" in v) {
65
+ const f = v;
66
+ const bytes = Buffer.from(f.data, "base64");
67
+ const blob = new Blob([bytes], { type: f.contentType || "application/octet-stream" });
68
+ form.append("file", blob, f.filename || "upload.bin");
69
+ }
70
+ else {
71
+ form.append(k, String(v));
72
+ }
73
+ }
74
+ payload = form;
75
+ // fetch sets multipart boundary automatically
76
+ }
77
+ else {
78
+ headers["Content-Type"] = "application/json";
79
+ payload = JSON.stringify(body);
80
+ }
81
+ }
82
+ const res = await fetch(url, { method, headers, body: payload });
83
+ if (!res.ok) {
84
+ throw new Error(`WhatsApp Cloud API ${res.status}: ${await res.text()}`);
85
+ }
86
+ const text = await res.text();
87
+ return text ? JSON.parse(text) : {};
88
+ }
89
+ const server = new Server({ name: "mcp-whatsapp-cloud", version: "0.1.0" }, { capabilities: { tools: {} } });
90
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
91
+ tools: [
92
+ {
93
+ name: "send_text_message",
94
+ description: "Send a plain text message. For business-initiated conversations outside the 24h customer-service window, use send_template_message instead.",
95
+ inputSchema: {
96
+ type: "object",
97
+ properties: {
98
+ to: { type: "string", description: "Recipient phone number in E.164 without + (e.g. 5511999999999)" },
99
+ body: { type: "string", description: "Message text (UTF-8, up to 4096 chars)" },
100
+ preview_url: { type: "boolean", description: "Enable URL preview for links in body (default false)" },
101
+ },
102
+ required: ["to", "body"],
103
+ },
104
+ },
105
+ {
106
+ name: "send_template_message",
107
+ description: "Send an approved message template. Required for business-initiated conversations. Templates must be pre-approved by Meta via create_template.",
108
+ inputSchema: {
109
+ type: "object",
110
+ properties: {
111
+ to: { type: "string", description: "Recipient phone number in E.164 without +" },
112
+ template_name: { type: "string", description: "Exact name of the approved template" },
113
+ language_code: { type: "string", description: "BCP-47 language tag (e.g. en_US, pt_BR, es_MX)" },
114
+ components: {
115
+ type: "array",
116
+ description: "Template component parameters (header, body, button). See Cloud API docs for shape.",
117
+ items: { type: "object" },
118
+ },
119
+ },
120
+ required: ["to", "template_name", "language_code"],
121
+ },
122
+ },
123
+ {
124
+ name: "send_media_message",
125
+ description: "Send an image, video, document, or audio. Supply either `link` (public URL) or `id` (media_id from upload_media).",
126
+ inputSchema: {
127
+ type: "object",
128
+ properties: {
129
+ to: { type: "string", description: "Recipient phone number in E.164 without +" },
130
+ media_type: { type: "string", enum: ["image", "video", "document", "audio"], description: "Kind of media" },
131
+ link: { type: "string", description: "Public HTTPS URL of the media. Use this OR id." },
132
+ id: { type: "string", description: "Uploaded media_id from upload_media. Use this OR link." },
133
+ caption: { type: "string", description: "Optional caption (image, video, document only)" },
134
+ filename: { type: "string", description: "Optional filename (document only)" },
135
+ },
136
+ required: ["to", "media_type"],
137
+ },
138
+ },
139
+ {
140
+ name: "send_interactive_message",
141
+ description: "Send an interactive message (reply buttons or list). Supply a fully-formed `interactive` object per Cloud API spec.",
142
+ inputSchema: {
143
+ type: "object",
144
+ properties: {
145
+ to: { type: "string", description: "Recipient phone number in E.164 without +" },
146
+ interactive: {
147
+ type: "object",
148
+ description: "Interactive payload. For buttons: { type: 'button', body: { text }, action: { buttons: [...] } }. For list: { type: 'list', body: { text }, action: { button, sections } }.",
149
+ },
150
+ },
151
+ required: ["to", "interactive"],
152
+ },
153
+ },
154
+ {
155
+ name: "send_location_message",
156
+ description: "Send a location pin with latitude/longitude and optional name/address.",
157
+ inputSchema: {
158
+ type: "object",
159
+ properties: {
160
+ to: { type: "string", description: "Recipient phone number in E.164 without +" },
161
+ latitude: { type: "number", description: "Latitude" },
162
+ longitude: { type: "number", description: "Longitude" },
163
+ name: { type: "string", description: "Optional location name" },
164
+ address: { type: "string", description: "Optional location address" },
165
+ },
166
+ required: ["to", "latitude", "longitude"],
167
+ },
168
+ },
169
+ {
170
+ name: "mark_message_as_read",
171
+ description: "Mark an incoming message as read so the sender sees the blue double-check. Uses the wamid from the inbound webhook.",
172
+ inputSchema: {
173
+ type: "object",
174
+ properties: {
175
+ message_id: { type: "string", description: "Inbound message id (wamid.XXX) from the webhook" },
176
+ },
177
+ required: ["message_id"],
178
+ },
179
+ },
180
+ {
181
+ name: "upload_media",
182
+ description: "Upload a media file and get back a media_id reusable in send_media_message. Multipart POST to /{phone_number_id}/media.",
183
+ inputSchema: {
184
+ type: "object",
185
+ properties: {
186
+ file_base64: { type: "string", description: "File contents, base64-encoded" },
187
+ filename: { type: "string", description: "Filename (used for display and extension inference)" },
188
+ mime_type: { type: "string", description: "MIME type (e.g. image/jpeg, video/mp4, application/pdf, audio/ogg)" },
189
+ },
190
+ required: ["file_base64", "filename", "mime_type"],
191
+ },
192
+ },
193
+ {
194
+ name: "retrieve_media_url",
195
+ description: "Resolve a media_id to a short-lived downloadable URL. The URL itself still requires the Bearer token to fetch.",
196
+ inputSchema: {
197
+ type: "object",
198
+ properties: {
199
+ media_id: { type: "string", description: "Media id returned by upload_media or received in a webhook" },
200
+ },
201
+ required: ["media_id"],
202
+ },
203
+ },
204
+ {
205
+ name: "delete_media",
206
+ description: "Delete an uploaded media asset by id.",
207
+ inputSchema: {
208
+ type: "object",
209
+ properties: {
210
+ media_id: { type: "string", description: "Media id to delete" },
211
+ },
212
+ required: ["media_id"],
213
+ },
214
+ },
215
+ {
216
+ name: "list_templates",
217
+ description: "List message templates on the WhatsApp Business Account. Supports optional paging and name filter.",
218
+ inputSchema: {
219
+ type: "object",
220
+ properties: {
221
+ limit: { type: "number", description: "Max templates per page (default 25)" },
222
+ name: { type: "string", description: "Filter by exact template name" },
223
+ status: { type: "string", description: "Filter by status (APPROVED, PENDING, REJECTED, PAUSED, DISABLED)" },
224
+ },
225
+ },
226
+ },
227
+ {
228
+ name: "create_template",
229
+ description: "Submit a new template for Meta review. Templates become usable only after approval (usually minutes to hours).",
230
+ inputSchema: {
231
+ type: "object",
232
+ properties: {
233
+ name: { type: "string", description: "Template name (lowercase, underscores, unique on WABA)" },
234
+ language: { type: "string", description: "BCP-47 language tag (e.g. en_US, pt_BR)" },
235
+ category: { type: "string", enum: ["AUTHENTICATION", "MARKETING", "UTILITY"], description: "Template category" },
236
+ components: {
237
+ type: "array",
238
+ description: "Array of components: HEADER, BODY, FOOTER, BUTTONS. See Cloud API template docs.",
239
+ items: { type: "object" },
240
+ },
241
+ allow_category_change: { type: "boolean", description: "Let Meta re-categorize if needed (default true recommended)" },
242
+ },
243
+ required: ["name", "language", "category", "components"],
244
+ },
245
+ },
246
+ ],
247
+ }));
248
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
249
+ const { name, arguments: args } = request.params;
250
+ const a = (args ?? {});
251
+ try {
252
+ switch (name) {
253
+ case "send_text_message": {
254
+ const body = {
255
+ messaging_product: "whatsapp",
256
+ recipient_type: "individual",
257
+ to: a.to,
258
+ type: "text",
259
+ text: {
260
+ body: a.body,
261
+ preview_url: a.preview_url ?? false,
262
+ },
263
+ };
264
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/messages`, body), null, 2) }] };
265
+ }
266
+ case "send_template_message": {
267
+ const template = {
268
+ name: a.template_name,
269
+ language: { code: a.language_code },
270
+ };
271
+ if (a.components)
272
+ template.components = a.components;
273
+ const body = {
274
+ messaging_product: "whatsapp",
275
+ to: a.to,
276
+ type: "template",
277
+ template,
278
+ };
279
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/messages`, body), null, 2) }] };
280
+ }
281
+ case "send_media_message": {
282
+ const mediaType = String(a.media_type);
283
+ const mediaObj = {};
284
+ if (a.id)
285
+ mediaObj.id = a.id;
286
+ if (a.link)
287
+ mediaObj.link = a.link;
288
+ if (a.caption && mediaType !== "audio")
289
+ mediaObj.caption = a.caption;
290
+ if (a.filename && mediaType === "document")
291
+ mediaObj.filename = a.filename;
292
+ const body = {
293
+ messaging_product: "whatsapp",
294
+ recipient_type: "individual",
295
+ to: a.to,
296
+ type: mediaType,
297
+ [mediaType]: mediaObj,
298
+ };
299
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/messages`, body), null, 2) }] };
300
+ }
301
+ case "send_interactive_message": {
302
+ const body = {
303
+ messaging_product: "whatsapp",
304
+ recipient_type: "individual",
305
+ to: a.to,
306
+ type: "interactive",
307
+ interactive: a.interactive,
308
+ };
309
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/messages`, body), null, 2) }] };
310
+ }
311
+ case "send_location_message": {
312
+ const location = {
313
+ latitude: a.latitude,
314
+ longitude: a.longitude,
315
+ };
316
+ if (a.name)
317
+ location.name = a.name;
318
+ if (a.address)
319
+ location.address = a.address;
320
+ const body = {
321
+ messaging_product: "whatsapp",
322
+ recipient_type: "individual",
323
+ to: a.to,
324
+ type: "location",
325
+ location,
326
+ };
327
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/messages`, body), null, 2) }] };
328
+ }
329
+ case "mark_message_as_read": {
330
+ const body = {
331
+ messaging_product: "whatsapp",
332
+ status: "read",
333
+ message_id: a.message_id,
334
+ };
335
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/messages`, body), null, 2) }] };
336
+ }
337
+ case "upload_media": {
338
+ const body = {
339
+ messaging_product: "whatsapp",
340
+ type: a.mime_type,
341
+ file: {
342
+ data: a.file_base64,
343
+ filename: a.filename,
344
+ contentType: a.mime_type,
345
+ },
346
+ };
347
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/media`, body, { multipart: true }), null, 2) }] };
348
+ }
349
+ case "retrieve_media_url":
350
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("GET", `/${a.media_id}`), null, 2) }] };
351
+ case "delete_media":
352
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("DELETE", `/${a.media_id}`), null, 2) }] };
353
+ case "list_templates": {
354
+ const q = new URLSearchParams();
355
+ if (a.limit !== undefined)
356
+ q.set("limit", String(a.limit));
357
+ if (a.name)
358
+ q.set("name", String(a.name));
359
+ if (a.status)
360
+ q.set("status", String(a.status));
361
+ const qs = q.toString();
362
+ const path = `/${BUSINESS_ACCOUNT_ID}/message_templates${qs ? `?${qs}` : ""}`;
363
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("GET", path), null, 2) }] };
364
+ }
365
+ case "create_template": {
366
+ const body = {
367
+ name: a.name,
368
+ language: a.language,
369
+ category: a.category,
370
+ components: a.components,
371
+ };
372
+ if (a.allow_category_change !== undefined)
373
+ body.allow_category_change = a.allow_category_change;
374
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${BUSINESS_ACCOUNT_ID}/message_templates`, body), null, 2) }] };
375
+ }
376
+ default:
377
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
378
+ }
379
+ }
380
+ catch (err) {
381
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
382
+ }
383
+ });
384
+ async function main() {
385
+ if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
386
+ const { default: express } = await import("express");
387
+ const { randomUUID } = await import("node:crypto");
388
+ const app = express();
389
+ app.use(express.json());
390
+ const transports = new Map();
391
+ app.get("/health", (_req, res) => res.json({ status: "ok", sessions: transports.size }));
392
+ app.post("/mcp", async (req, res) => {
393
+ const sid = req.headers["mcp-session-id"];
394
+ if (sid && transports.has(sid)) {
395
+ await transports.get(sid).handleRequest(req, res, req.body);
396
+ return;
397
+ }
398
+ if (!sid && isInitializeRequest(req.body)) {
399
+ const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
400
+ t.onclose = () => { if (t.sessionId)
401
+ transports.delete(t.sessionId); };
402
+ const s = new Server({ name: "mcp-whatsapp-cloud", version: "0.1.0" }, { capabilities: { tools: {} } });
403
+ server._requestHandlers.forEach((v, k) => s._requestHandlers.set(k, v));
404
+ server._notificationHandlers?.forEach((v, k) => s._notificationHandlers.set(k, v));
405
+ await s.connect(t);
406
+ await t.handleRequest(req, res, req.body);
407
+ return;
408
+ }
409
+ res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
410
+ });
411
+ app.get("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
412
+ await transports.get(sid).handleRequest(req, res);
413
+ else
414
+ res.status(400).send("Invalid session"); });
415
+ app.delete("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
416
+ await transports.get(sid).handleRequest(req, res);
417
+ else
418
+ res.status(400).send("Invalid session"); });
419
+ const port = Number(process.env.MCP_PORT) || 3000;
420
+ app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
421
+ }
422
+ else {
423
+ const transport = new StdioServerTransport();
424
+ await server.connect(transport);
425
+ }
426
+ }
427
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@codespar/mcp-whatsapp-cloud",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for WhatsApp Cloud API (Meta direct) — official Graph API integration for messages, media, and templates",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "bin": {
8
+ "mcp-whatsapp-cloud": "./dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "node dist/index.js"
13
+ },
14
+ "dependencies": {
15
+ "@modelcontextprotocol/sdk": "^1.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^25.5.0",
19
+ "typescript": "^5.8.0"
20
+ },
21
+ "license": "MIT",
22
+ "keywords": [
23
+ "mcp",
24
+ "whatsapp",
25
+ "whatsapp-cloud",
26
+ "meta",
27
+ "graph-api",
28
+ "messaging",
29
+ "waba",
30
+ "business"
31
+ ],
32
+ "mcpName": "io.github.codespar/mcp-whatsapp-cloud"
33
+ }
package/server.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.codespar/mcp-whatsapp-cloud",
4
+ "description": "MCP server for WhatsApp Cloud API (Meta direct) — official Graph API integration for messages, media, and templates",
5
+ "repository": {
6
+ "url": "https://github.com/codespar/mcp-dev-brasil",
7
+ "source": "github",
8
+ "subfolder": "packages/communication/whatsapp-cloud"
9
+ },
10
+ "version": "0.1.0",
11
+ "packages": [
12
+ {
13
+ "registryType": "npm",
14
+ "identifier": "@codespar/mcp-whatsapp-cloud",
15
+ "version": "0.1.0",
16
+ "transport": {
17
+ "type": "stdio"
18
+ },
19
+ "environmentVariables": [
20
+ {
21
+ "name": "WHATSAPP_ACCESS_TOKEN",
22
+ "description": "Meta system-user access token (permanent). Used as Bearer token for Graph API.",
23
+ "isRequired": true,
24
+ "format": "string",
25
+ "isSecret": true
26
+ },
27
+ {
28
+ "name": "WHATSAPP_PHONE_NUMBER_ID",
29
+ "description": "WhatsApp Business phone number ID registered with Meta.",
30
+ "isRequired": true,
31
+ "format": "string",
32
+ "isSecret": false
33
+ },
34
+ {
35
+ "name": "WHATSAPP_BUSINESS_ACCOUNT_ID",
36
+ "description": "WhatsApp Business Account (WABA) ID. Used for template management.",
37
+ "isRequired": true,
38
+ "format": "string",
39
+ "isSecret": false
40
+ },
41
+ {
42
+ "name": "WHATSAPP_GRAPH_VERSION",
43
+ "description": "Graph API version. Defaults to v21.0. Meta bumps quarterly.",
44
+ "isRequired": false,
45
+ "format": "string",
46
+ "isSecret": false
47
+ }
48
+ ]
49
+ }
50
+ ]
51
+ }
package/src/index.ts ADDED
@@ -0,0 +1,427 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * MCP Server for WhatsApp Cloud API — Meta's official Business API.
5
+ *
6
+ * Direct integration with Meta Graph API. No middleman. Distinct from the
7
+ * wrapper providers in the catalog (Z-API, Evolution, Take Blip, Zenvia) which
8
+ * all sit on top of this. Target: merchants with an approved WhatsApp Business
9
+ * Account (WABA) who want Meta-direct pricing and full control.
10
+ *
11
+ * Tools (11):
12
+ * send_text_message — simple text message
13
+ * send_template_message — approved template (required for 24h+ business-initiated)
14
+ * send_media_message — image, video, document, or audio
15
+ * send_interactive_message — buttons or list
16
+ * send_location_message — latitude/longitude pin
17
+ * mark_message_as_read — mark an incoming message as read
18
+ * upload_media — upload a file and get a media_id (multipart)
19
+ * retrieve_media_url — resolve a media_id to a downloadable URL
20
+ * delete_media — delete an uploaded media asset
21
+ * list_templates — list templates on the WABA
22
+ * create_template — submit a new template for Meta review
23
+ *
24
+ * Authentication
25
+ * Bearer token (permanent system-user access token).
26
+ * Authorization: Bearer <WHATSAPP_ACCESS_TOKEN>
27
+ *
28
+ * API surface
29
+ * Graph API base: https://graph.facebook.com/{version}
30
+ * Message + media endpoints are scoped to a phone_number_id.
31
+ * Template endpoints are scoped to a business_account_id.
32
+ *
33
+ * Environment
34
+ * WHATSAPP_ACCESS_TOKEN required — Meta system-user token (secret)
35
+ * WHATSAPP_PHONE_NUMBER_ID required — WABA phone number id
36
+ * WHATSAPP_BUSINESS_ACCOUNT_ID required — WABA id (for template management)
37
+ * WHATSAPP_GRAPH_VERSION optional — default v21.0
38
+ *
39
+ * Docs: https://developers.facebook.com/docs/whatsapp/cloud-api
40
+ */
41
+
42
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
43
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
44
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
45
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
46
+ import {
47
+ CallToolRequestSchema,
48
+ ListToolsRequestSchema,
49
+ } from "@modelcontextprotocol/sdk/types.js";
50
+
51
+ const ACCESS_TOKEN = process.env.WHATSAPP_ACCESS_TOKEN || "";
52
+ const PHONE_NUMBER_ID = process.env.WHATSAPP_PHONE_NUMBER_ID || "";
53
+ const BUSINESS_ACCOUNT_ID = process.env.WHATSAPP_BUSINESS_ACCOUNT_ID || "";
54
+ const GRAPH_VERSION = process.env.WHATSAPP_GRAPH_VERSION || "v21.0";
55
+ const BASE_URL = `https://graph.facebook.com/${GRAPH_VERSION}`;
56
+
57
+ interface RequestOptions {
58
+ multipart?: boolean;
59
+ }
60
+
61
+ async function whatsappRequest(
62
+ method: string,
63
+ path: string,
64
+ body?: unknown,
65
+ opts: RequestOptions = {}
66
+ ): Promise<unknown> {
67
+ const url = `${BASE_URL}${path}`;
68
+ const headers: Record<string, string> = {
69
+ Authorization: `Bearer ${ACCESS_TOKEN}`,
70
+ Accept: "application/json",
71
+ };
72
+
73
+ let payload: BodyInit | undefined;
74
+ if (body !== undefined && body !== null) {
75
+ if (opts.multipart) {
76
+ const form = new FormData();
77
+ const b = body as Record<string, unknown>;
78
+ for (const [k, v] of Object.entries(b)) {
79
+ if (v === undefined || v === null) continue;
80
+ if (k === "file" && typeof v === "object" && v !== null && "data" in (v as Record<string, unknown>)) {
81
+ const f = v as { data: string; filename?: string; contentType?: string };
82
+ const bytes = Buffer.from(f.data, "base64");
83
+ const blob = new Blob([bytes], { type: f.contentType || "application/octet-stream" });
84
+ form.append("file", blob, f.filename || "upload.bin");
85
+ } else {
86
+ form.append(k, String(v));
87
+ }
88
+ }
89
+ payload = form;
90
+ // fetch sets multipart boundary automatically
91
+ } else {
92
+ headers["Content-Type"] = "application/json";
93
+ payload = JSON.stringify(body);
94
+ }
95
+ }
96
+
97
+ const res = await fetch(url, { method, headers, body: payload });
98
+ if (!res.ok) {
99
+ throw new Error(`WhatsApp Cloud API ${res.status}: ${await res.text()}`);
100
+ }
101
+ const text = await res.text();
102
+ return text ? JSON.parse(text) : {};
103
+ }
104
+
105
+ const server = new Server(
106
+ { name: "mcp-whatsapp-cloud", version: "0.1.0" },
107
+ { capabilities: { tools: {} } }
108
+ );
109
+
110
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
111
+ tools: [
112
+ {
113
+ name: "send_text_message",
114
+ description: "Send a plain text message. For business-initiated conversations outside the 24h customer-service window, use send_template_message instead.",
115
+ inputSchema: {
116
+ type: "object",
117
+ properties: {
118
+ to: { type: "string", description: "Recipient phone number in E.164 without + (e.g. 5511999999999)" },
119
+ body: { type: "string", description: "Message text (UTF-8, up to 4096 chars)" },
120
+ preview_url: { type: "boolean", description: "Enable URL preview for links in body (default false)" },
121
+ },
122
+ required: ["to", "body"],
123
+ },
124
+ },
125
+ {
126
+ name: "send_template_message",
127
+ description: "Send an approved message template. Required for business-initiated conversations. Templates must be pre-approved by Meta via create_template.",
128
+ inputSchema: {
129
+ type: "object",
130
+ properties: {
131
+ to: { type: "string", description: "Recipient phone number in E.164 without +" },
132
+ template_name: { type: "string", description: "Exact name of the approved template" },
133
+ language_code: { type: "string", description: "BCP-47 language tag (e.g. en_US, pt_BR, es_MX)" },
134
+ components: {
135
+ type: "array",
136
+ description: "Template component parameters (header, body, button). See Cloud API docs for shape.",
137
+ items: { type: "object" },
138
+ },
139
+ },
140
+ required: ["to", "template_name", "language_code"],
141
+ },
142
+ },
143
+ {
144
+ name: "send_media_message",
145
+ description: "Send an image, video, document, or audio. Supply either `link` (public URL) or `id` (media_id from upload_media).",
146
+ inputSchema: {
147
+ type: "object",
148
+ properties: {
149
+ to: { type: "string", description: "Recipient phone number in E.164 without +" },
150
+ media_type: { type: "string", enum: ["image", "video", "document", "audio"], description: "Kind of media" },
151
+ link: { type: "string", description: "Public HTTPS URL of the media. Use this OR id." },
152
+ id: { type: "string", description: "Uploaded media_id from upload_media. Use this OR link." },
153
+ caption: { type: "string", description: "Optional caption (image, video, document only)" },
154
+ filename: { type: "string", description: "Optional filename (document only)" },
155
+ },
156
+ required: ["to", "media_type"],
157
+ },
158
+ },
159
+ {
160
+ name: "send_interactive_message",
161
+ description: "Send an interactive message (reply buttons or list). Supply a fully-formed `interactive` object per Cloud API spec.",
162
+ inputSchema: {
163
+ type: "object",
164
+ properties: {
165
+ to: { type: "string", description: "Recipient phone number in E.164 without +" },
166
+ interactive: {
167
+ type: "object",
168
+ description: "Interactive payload. For buttons: { type: 'button', body: { text }, action: { buttons: [...] } }. For list: { type: 'list', body: { text }, action: { button, sections } }.",
169
+ },
170
+ },
171
+ required: ["to", "interactive"],
172
+ },
173
+ },
174
+ {
175
+ name: "send_location_message",
176
+ description: "Send a location pin with latitude/longitude and optional name/address.",
177
+ inputSchema: {
178
+ type: "object",
179
+ properties: {
180
+ to: { type: "string", description: "Recipient phone number in E.164 without +" },
181
+ latitude: { type: "number", description: "Latitude" },
182
+ longitude: { type: "number", description: "Longitude" },
183
+ name: { type: "string", description: "Optional location name" },
184
+ address: { type: "string", description: "Optional location address" },
185
+ },
186
+ required: ["to", "latitude", "longitude"],
187
+ },
188
+ },
189
+ {
190
+ name: "mark_message_as_read",
191
+ description: "Mark an incoming message as read so the sender sees the blue double-check. Uses the wamid from the inbound webhook.",
192
+ inputSchema: {
193
+ type: "object",
194
+ properties: {
195
+ message_id: { type: "string", description: "Inbound message id (wamid.XXX) from the webhook" },
196
+ },
197
+ required: ["message_id"],
198
+ },
199
+ },
200
+ {
201
+ name: "upload_media",
202
+ description: "Upload a media file and get back a media_id reusable in send_media_message. Multipart POST to /{phone_number_id}/media.",
203
+ inputSchema: {
204
+ type: "object",
205
+ properties: {
206
+ file_base64: { type: "string", description: "File contents, base64-encoded" },
207
+ filename: { type: "string", description: "Filename (used for display and extension inference)" },
208
+ mime_type: { type: "string", description: "MIME type (e.g. image/jpeg, video/mp4, application/pdf, audio/ogg)" },
209
+ },
210
+ required: ["file_base64", "filename", "mime_type"],
211
+ },
212
+ },
213
+ {
214
+ name: "retrieve_media_url",
215
+ description: "Resolve a media_id to a short-lived downloadable URL. The URL itself still requires the Bearer token to fetch.",
216
+ inputSchema: {
217
+ type: "object",
218
+ properties: {
219
+ media_id: { type: "string", description: "Media id returned by upload_media or received in a webhook" },
220
+ },
221
+ required: ["media_id"],
222
+ },
223
+ },
224
+ {
225
+ name: "delete_media",
226
+ description: "Delete an uploaded media asset by id.",
227
+ inputSchema: {
228
+ type: "object",
229
+ properties: {
230
+ media_id: { type: "string", description: "Media id to delete" },
231
+ },
232
+ required: ["media_id"],
233
+ },
234
+ },
235
+ {
236
+ name: "list_templates",
237
+ description: "List message templates on the WhatsApp Business Account. Supports optional paging and name filter.",
238
+ inputSchema: {
239
+ type: "object",
240
+ properties: {
241
+ limit: { type: "number", description: "Max templates per page (default 25)" },
242
+ name: { type: "string", description: "Filter by exact template name" },
243
+ status: { type: "string", description: "Filter by status (APPROVED, PENDING, REJECTED, PAUSED, DISABLED)" },
244
+ },
245
+ },
246
+ },
247
+ {
248
+ name: "create_template",
249
+ description: "Submit a new template for Meta review. Templates become usable only after approval (usually minutes to hours).",
250
+ inputSchema: {
251
+ type: "object",
252
+ properties: {
253
+ name: { type: "string", description: "Template name (lowercase, underscores, unique on WABA)" },
254
+ language: { type: "string", description: "BCP-47 language tag (e.g. en_US, pt_BR)" },
255
+ category: { type: "string", enum: ["AUTHENTICATION", "MARKETING", "UTILITY"], description: "Template category" },
256
+ components: {
257
+ type: "array",
258
+ description: "Array of components: HEADER, BODY, FOOTER, BUTTONS. See Cloud API template docs.",
259
+ items: { type: "object" },
260
+ },
261
+ allow_category_change: { type: "boolean", description: "Let Meta re-categorize if needed (default true recommended)" },
262
+ },
263
+ required: ["name", "language", "category", "components"],
264
+ },
265
+ },
266
+ ],
267
+ }));
268
+
269
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
270
+ const { name, arguments: args } = request.params;
271
+ const a = (args ?? {}) as Record<string, unknown>;
272
+
273
+ try {
274
+ switch (name) {
275
+ case "send_text_message": {
276
+ const body = {
277
+ messaging_product: "whatsapp",
278
+ recipient_type: "individual",
279
+ to: a.to,
280
+ type: "text",
281
+ text: {
282
+ body: a.body,
283
+ preview_url: a.preview_url ?? false,
284
+ },
285
+ };
286
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/messages`, body), null, 2) }] };
287
+ }
288
+ case "send_template_message": {
289
+ const template: Record<string, unknown> = {
290
+ name: a.template_name,
291
+ language: { code: a.language_code },
292
+ };
293
+ if (a.components) template.components = a.components;
294
+ const body = {
295
+ messaging_product: "whatsapp",
296
+ to: a.to,
297
+ type: "template",
298
+ template,
299
+ };
300
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/messages`, body), null, 2) }] };
301
+ }
302
+ case "send_media_message": {
303
+ const mediaType = String(a.media_type);
304
+ const mediaObj: Record<string, unknown> = {};
305
+ if (a.id) mediaObj.id = a.id;
306
+ if (a.link) mediaObj.link = a.link;
307
+ if (a.caption && mediaType !== "audio") mediaObj.caption = a.caption;
308
+ if (a.filename && mediaType === "document") mediaObj.filename = a.filename;
309
+ const body = {
310
+ messaging_product: "whatsapp",
311
+ recipient_type: "individual",
312
+ to: a.to,
313
+ type: mediaType,
314
+ [mediaType]: mediaObj,
315
+ };
316
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/messages`, body), null, 2) }] };
317
+ }
318
+ case "send_interactive_message": {
319
+ const body = {
320
+ messaging_product: "whatsapp",
321
+ recipient_type: "individual",
322
+ to: a.to,
323
+ type: "interactive",
324
+ interactive: a.interactive,
325
+ };
326
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/messages`, body), null, 2) }] };
327
+ }
328
+ case "send_location_message": {
329
+ const location: Record<string, unknown> = {
330
+ latitude: a.latitude,
331
+ longitude: a.longitude,
332
+ };
333
+ if (a.name) location.name = a.name;
334
+ if (a.address) location.address = a.address;
335
+ const body = {
336
+ messaging_product: "whatsapp",
337
+ recipient_type: "individual",
338
+ to: a.to,
339
+ type: "location",
340
+ location,
341
+ };
342
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/messages`, body), null, 2) }] };
343
+ }
344
+ case "mark_message_as_read": {
345
+ const body = {
346
+ messaging_product: "whatsapp",
347
+ status: "read",
348
+ message_id: a.message_id,
349
+ };
350
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/messages`, body), null, 2) }] };
351
+ }
352
+ case "upload_media": {
353
+ const body = {
354
+ messaging_product: "whatsapp",
355
+ type: a.mime_type,
356
+ file: {
357
+ data: a.file_base64,
358
+ filename: a.filename,
359
+ contentType: a.mime_type,
360
+ },
361
+ };
362
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/media`, body, { multipart: true }), null, 2) }] };
363
+ }
364
+ case "retrieve_media_url":
365
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("GET", `/${a.media_id}`), null, 2) }] };
366
+ case "delete_media":
367
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("DELETE", `/${a.media_id}`), null, 2) }] };
368
+ case "list_templates": {
369
+ const q = new URLSearchParams();
370
+ if (a.limit !== undefined) q.set("limit", String(a.limit));
371
+ if (a.name) q.set("name", String(a.name));
372
+ if (a.status) q.set("status", String(a.status));
373
+ const qs = q.toString();
374
+ const path = `/${BUSINESS_ACCOUNT_ID}/message_templates${qs ? `?${qs}` : ""}`;
375
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("GET", path), null, 2) }] };
376
+ }
377
+ case "create_template": {
378
+ const body: Record<string, unknown> = {
379
+ name: a.name,
380
+ language: a.language,
381
+ category: a.category,
382
+ components: a.components,
383
+ };
384
+ if (a.allow_category_change !== undefined) body.allow_category_change = a.allow_category_change;
385
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${BUSINESS_ACCOUNT_ID}/message_templates`, body), null, 2) }] };
386
+ }
387
+ default:
388
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
389
+ }
390
+ } catch (err) {
391
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
392
+ }
393
+ });
394
+
395
+ async function main() {
396
+ if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
397
+ const { default: express } = await import("express");
398
+ const { randomUUID } = await import("node:crypto");
399
+ const app = express();
400
+ app.use(express.json());
401
+ const transports = new Map<string, StreamableHTTPServerTransport>();
402
+ app.get("/health", (_req: unknown, res: { json: (body: unknown) => unknown }) => res.json({ status: "ok", sessions: transports.size }));
403
+ app.post("/mcp", async (req: { headers: Record<string, string | string[] | undefined>; body: unknown }, res: { status: (code: number) => { json: (body: unknown) => unknown } }) => {
404
+ const sid = req.headers["mcp-session-id"] as string | undefined;
405
+ if (sid && transports.has(sid)) { await transports.get(sid)!.handleRequest(req as never, res as never, req.body); return; }
406
+ if (!sid && isInitializeRequest(req.body)) {
407
+ const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
408
+ t.onclose = () => { if (t.sessionId) transports.delete(t.sessionId); };
409
+ const s = new Server({ name: "mcp-whatsapp-cloud", version: "0.1.0" }, { capabilities: { tools: {} } });
410
+ (server as unknown as { _requestHandlers: Map<unknown, unknown> })._requestHandlers.forEach((v, k) => (s as unknown as { _requestHandlers: Map<unknown, unknown> })._requestHandlers.set(k, v));
411
+ (server as unknown as { _notificationHandlers?: Map<unknown, unknown> })._notificationHandlers?.forEach((v, k) => (s as unknown as { _notificationHandlers: Map<unknown, unknown> })._notificationHandlers.set(k, v));
412
+ await s.connect(t);
413
+ await t.handleRequest(req as never, res as never, req.body); return;
414
+ }
415
+ res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
416
+ });
417
+ app.get("/mcp", async (req: { headers: Record<string, string | string[] | undefined> }, res: { status: (code: number) => { send: (body: string) => unknown } }) => { const sid = req.headers["mcp-session-id"] as string; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req as never, res as never); else res.status(400).send("Invalid session"); });
418
+ app.delete("/mcp", async (req: { headers: Record<string, string | string[] | undefined> }, res: { status: (code: number) => { send: (body: string) => unknown } }) => { const sid = req.headers["mcp-session-id"] as string; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req as never, res as never); else res.status(400).send("Invalid session"); });
419
+ const port = Number(process.env.MCP_PORT) || 3000;
420
+ app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
421
+ } else {
422
+ const transport = new StdioServerTransport();
423
+ await server.connect(transport);
424
+ }
425
+ }
426
+
427
+ main().catch(console.error);
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "esModuleInterop": true,
11
+ "declaration": true
12
+ },
13
+ "include": ["src"]
14
+ }