@codespar/mcp-twilio 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,76 @@
1
+ # @codespar/mcp-twilio
2
+
3
+ MCP server for [Twilio](https://www.twilio.com) — the global standard for programmable messaging and voice.
4
+
5
+ SMS, WhatsApp, and Voice across 180+ countries. Verify (2FA) and Lookup (phone validation) included. Fills the global messaging gap in a catalog otherwise tilted to Brazil-specific providers (Z-API, Take Blip, Zenvia, Evolution API).
6
+
7
+ ## Tools
8
+
9
+ | Tool | Purpose |
10
+ |------|---------|
11
+ | `send_message` | Send an SMS or WhatsApp message (prefix `To` with `whatsapp:+E164` for WhatsApp) |
12
+ | `get_message` | Retrieve a message by SID |
13
+ | `list_messages` | List messages with optional filters (To, From, DateSent) |
14
+ | `delete_message` | Delete a message from history |
15
+ | `make_call` | Place an outbound voice call driven by a TwiML `Url` |
16
+ | `get_call` | Retrieve a call by SID |
17
+ | `update_call` | Hang up or redirect an in-progress call |
18
+ | `start_verification` | Send a Verify (2FA) code via sms / whatsapp / call |
19
+ | `check_verification` | Check a Verify (2FA) code |
20
+ | `lookup_phone` | Validate + format + classify a phone number (Lookups v2) |
21
+ | `list_incoming_numbers` | List your Twilio-provisioned phone numbers |
22
+ | `buy_phone_number` | Provision a new phone number |
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ npm install @codespar/mcp-twilio
28
+ ```
29
+
30
+ ## Environment
31
+
32
+ ```bash
33
+ TWILIO_ACCOUNT_SID="AC..." # required
34
+ TWILIO_AUTH_TOKEN="..." # required (secret)
35
+ TWILIO_MESSAGING_SERVICE_SID="MG..." # optional; default sender for send_message
36
+ ```
37
+
38
+ ## Authentication
39
+
40
+ HTTP Basic auth with `AccountSid:AuthToken`. The server handles this automatically — you only configure the env vars.
41
+
42
+ ```
43
+ Authorization: Basic <base64(AccountSid:AuthToken)>
44
+ ```
45
+
46
+ ## API surface
47
+
48
+ - Main Accounts API: `https://api.twilio.com/2010-04-01/Accounts/{AccountSid}` — Messages, Calls, IncomingPhoneNumbers
49
+ - Verify API: `https://verify.twilio.com/v2` — 2FA flows (requires a Verify Service SID passed per call)
50
+ - Lookups API: `https://lookups.twilio.com/v2` — phone number validation / carrier / line type
51
+
52
+ Request bodies are `application/x-www-form-urlencoded`; responses are JSON (endpoints use the `.json` suffix on the Accounts API).
53
+
54
+ ## WhatsApp
55
+
56
+ Use the same `send_message` tool, but prefix numbers with `whatsapp:`:
57
+
58
+ ```json
59
+ { "To": "whatsapp:+5511999999999", "From": "whatsapp:+14155238886", "Body": "Olá" }
60
+ ```
61
+
62
+ Sandbox numbers or approved WhatsApp-enabled senders work the same way.
63
+
64
+ ## Run
65
+
66
+ ```bash
67
+ # stdio (default — for Claude Desktop, Cursor, etc)
68
+ npx @codespar/mcp-twilio
69
+
70
+ # HTTP (for server-to-server testing)
71
+ MCP_HTTP=true MCP_PORT=3000 npx @codespar/mcp-twilio
72
+ ```
73
+
74
+ ## License
75
+
76
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,424 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP Server for Twilio — the global standard for programmable messaging + voice.
4
+ *
5
+ * One server covering Twilio's three most-used products:
6
+ * - Programmable Messaging (SMS + WhatsApp, same API, `whatsapp:` prefix)
7
+ * - Programmable Voice (outbound calls, hangup, redirect)
8
+ * - Verify (2FA one-time codes) + Lookups (phone validation)
9
+ *
10
+ * Fills the global-messaging gap in a catalog otherwise tilted to BR-specific
11
+ * providers (Z-API, Take Blip, Zenvia, Evolution API). Twilio ships in 180+
12
+ * countries on day one.
13
+ *
14
+ * Tools (12):
15
+ * send_message — send SMS or WhatsApp (prefix `To` with `whatsapp:+E164`)
16
+ * get_message — retrieve a message by SID
17
+ * list_messages — list messages with optional To/From/DateSent filters
18
+ * delete_message — delete a message from history
19
+ * make_call — place an outbound voice call driven by a TwiML Url
20
+ * get_call — retrieve a call by SID
21
+ * update_call — hang up or redirect an in-progress call
22
+ * start_verification — send a Verify (2FA) code via sms / whatsapp / call
23
+ * check_verification — check a Verify (2FA) code
24
+ * lookup_phone — validate + format + classify a phone number
25
+ * list_incoming_numbers — list provisioned Twilio phone numbers
26
+ * buy_phone_number — provision a new phone number
27
+ *
28
+ * Authentication
29
+ * HTTP Basic with AccountSid:AuthToken.
30
+ * Authorization: Basic <base64(AccountSid:AuthToken)>
31
+ *
32
+ * API surface
33
+ * Accounts API : https://api.twilio.com/2010-04-01/Accounts/{AccountSid}
34
+ * Verify API : https://verify.twilio.com/v2
35
+ * Lookups API : https://lookups.twilio.com/v2
36
+ *
37
+ * Request bodies are application/x-www-form-urlencoded. Responses are JSON
38
+ * (Accounts endpoints use the `.json` suffix).
39
+ *
40
+ * Environment
41
+ * TWILIO_ACCOUNT_SID required — Account SID (AC...)
42
+ * TWILIO_AUTH_TOKEN required — Auth Token (secret)
43
+ * TWILIO_MESSAGING_SERVICE_SID optional — default sender for send_message (MG...)
44
+ *
45
+ * Docs: https://www.twilio.com/docs/api
46
+ */
47
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
48
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
49
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
50
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
51
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
52
+ const ACCOUNT_SID = process.env.TWILIO_ACCOUNT_SID || "";
53
+ const AUTH_TOKEN = process.env.TWILIO_AUTH_TOKEN || "";
54
+ const DEFAULT_MESSAGING_SERVICE_SID = process.env.TWILIO_MESSAGING_SERVICE_SID || "";
55
+ const ACCOUNTS_BASE = `https://api.twilio.com/2010-04-01/Accounts/${ACCOUNT_SID}`;
56
+ async function twilioRequest(method, fullUrlOrPath, body) {
57
+ const url = fullUrlOrPath.startsWith("https://")
58
+ ? fullUrlOrPath
59
+ : `${ACCOUNTS_BASE}${fullUrlOrPath}`;
60
+ const basic = Buffer.from(`${ACCOUNT_SID}:${AUTH_TOKEN}`).toString("base64");
61
+ const headers = {
62
+ "Authorization": `Basic ${basic}`,
63
+ "Accept": "application/json",
64
+ };
65
+ let encodedBody;
66
+ if (body && Object.keys(body).length > 0) {
67
+ const params = new URLSearchParams();
68
+ for (const [k, v] of Object.entries(body)) {
69
+ if (v === undefined || v === null)
70
+ continue;
71
+ if (Array.isArray(v)) {
72
+ for (const item of v)
73
+ params.append(k, String(item));
74
+ }
75
+ else {
76
+ params.append(k, String(v));
77
+ }
78
+ }
79
+ encodedBody = params.toString();
80
+ headers["Content-Type"] = "application/x-www-form-urlencoded";
81
+ }
82
+ const res = await fetch(url, { method, headers, body: encodedBody });
83
+ if (!res.ok) {
84
+ throw new Error(`Twilio API ${res.status}: ${await res.text()}`);
85
+ }
86
+ const text = await res.text();
87
+ return text ? JSON.parse(text) : {};
88
+ }
89
+ function buildQuery(params) {
90
+ const q = new URLSearchParams();
91
+ for (const [k, v] of Object.entries(params)) {
92
+ if (v === undefined || v === null || v === "")
93
+ continue;
94
+ q.set(k, String(v));
95
+ }
96
+ const s = q.toString();
97
+ return s ? `?${s}` : "";
98
+ }
99
+ const server = new Server({ name: "mcp-twilio", version: "0.1.0" }, { capabilities: { tools: {} } });
100
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
101
+ tools: [
102
+ {
103
+ name: "send_message",
104
+ description: "Send an SMS or WhatsApp message. For WhatsApp, prefix `To` (and `From`) with `whatsapp:+E164`. Supply either `From` (a Twilio phone number) OR `MessagingServiceSid` (a Messaging Service). If neither is given, falls back to env TWILIO_MESSAGING_SERVICE_SID.",
105
+ inputSchema: {
106
+ type: "object",
107
+ properties: {
108
+ To: { type: "string", description: "Destination in E.164 (e.g. +5511999999999) or `whatsapp:+E164` for WhatsApp" },
109
+ From: { type: "string", description: "Twilio phone number in E.164, or `whatsapp:+E164` for WhatsApp. Omit if using MessagingServiceSid." },
110
+ MessagingServiceSid: { type: "string", description: "Messaging Service SID (MG...). Overrides env default. Omit if using From." },
111
+ Body: { type: "string", description: "Message text (UTF-8)" },
112
+ MediaUrl: { type: "array", items: { type: "string" }, description: "Optional list of media URLs (MMS / WhatsApp media)" },
113
+ StatusCallback: { type: "string", description: "Webhook URL Twilio calls on delivery-status transitions" },
114
+ },
115
+ required: ["To", "Body"],
116
+ },
117
+ },
118
+ {
119
+ name: "get_message",
120
+ description: "Retrieve a message resource by SID (SM... or MM...).",
121
+ inputSchema: {
122
+ type: "object",
123
+ properties: {
124
+ Sid: { type: "string", description: "Message SID" },
125
+ },
126
+ required: ["Sid"],
127
+ },
128
+ },
129
+ {
130
+ name: "list_messages",
131
+ description: "List messages with optional filters. Returns Twilio's paginated list; pass PageSize to cap.",
132
+ inputSchema: {
133
+ type: "object",
134
+ properties: {
135
+ To: { type: "string", description: "Filter by destination (E.164 or whatsapp:+E164)" },
136
+ From: { type: "string", description: "Filter by sender" },
137
+ DateSent: { type: "string", description: "Filter by exact send date (YYYY-MM-DD). Use DateSentAfter / DateSentBefore for ranges." },
138
+ DateSentAfter: { type: "string", description: "Return messages sent on/after this date (YYYY-MM-DD)" },
139
+ DateSentBefore: { type: "string", description: "Return messages sent on/before this date (YYYY-MM-DD)" },
140
+ PageSize: { type: "number", description: "Max rows per page (default 50, max 1000)" },
141
+ },
142
+ },
143
+ },
144
+ {
145
+ name: "delete_message",
146
+ description: "Delete a message from history. Irreversible.",
147
+ inputSchema: {
148
+ type: "object",
149
+ properties: {
150
+ Sid: { type: "string", description: "Message SID" },
151
+ },
152
+ required: ["Sid"],
153
+ },
154
+ },
155
+ {
156
+ name: "make_call",
157
+ description: "Place an outbound voice call. Twilio fetches TwiML from `Url` on connect to drive the call.",
158
+ inputSchema: {
159
+ type: "object",
160
+ properties: {
161
+ To: { type: "string", description: "Destination number in E.164" },
162
+ From: { type: "string", description: "Twilio-provisioned caller ID in E.164" },
163
+ Url: { type: "string", description: "HTTP(S) URL returning TwiML that drives the call" },
164
+ Method: { type: "string", enum: ["GET", "POST"], description: "HTTP method Twilio uses to fetch Url (default POST)" },
165
+ StatusCallback: { type: "string", description: "Webhook URL for call-status events" },
166
+ StatusCallbackEvent: { type: "array", items: { type: "string" }, description: "Events to subscribe to: initiated, ringing, answered, completed" },
167
+ },
168
+ required: ["To", "From", "Url"],
169
+ },
170
+ },
171
+ {
172
+ name: "get_call",
173
+ description: "Retrieve a call resource by SID (CA...).",
174
+ inputSchema: {
175
+ type: "object",
176
+ properties: {
177
+ Sid: { type: "string", description: "Call SID" },
178
+ },
179
+ required: ["Sid"],
180
+ },
181
+ },
182
+ {
183
+ name: "update_call",
184
+ description: "Modify an in-progress call. Set Status='completed' to hang up, or pass a new Url to redirect the call to fresh TwiML.",
185
+ inputSchema: {
186
+ type: "object",
187
+ properties: {
188
+ Sid: { type: "string", description: "Call SID" },
189
+ Status: { type: "string", enum: ["canceled", "completed"], description: "canceled (before answered) or completed (hang up)" },
190
+ Url: { type: "string", description: "New TwiML URL — redirects the live call" },
191
+ Method: { type: "string", enum: ["GET", "POST"], description: "HTTP method for the new Url" },
192
+ },
193
+ required: ["Sid"],
194
+ },
195
+ },
196
+ {
197
+ name: "start_verification",
198
+ description: "Start a Verify (2FA) challenge. Sends a one-time code to `To` via the chosen channel. Requires a Verify Service SID (VA...).",
199
+ inputSchema: {
200
+ type: "object",
201
+ properties: {
202
+ ServiceSid: { type: "string", description: "Verify Service SID (VA...)" },
203
+ To: { type: "string", description: "Destination in E.164 (or email for email channel)" },
204
+ Channel: { type: "string", enum: ["sms", "whatsapp", "call", "email"], description: "Delivery channel" },
205
+ Locale: { type: "string", description: "Message locale (e.g. pt-br, en, es)" },
206
+ },
207
+ required: ["ServiceSid", "To", "Channel"],
208
+ },
209
+ },
210
+ {
211
+ name: "check_verification",
212
+ description: "Check a Verify (2FA) code against a Service SID. Returns status=approved when the code matches.",
213
+ inputSchema: {
214
+ type: "object",
215
+ properties: {
216
+ ServiceSid: { type: "string", description: "Verify Service SID (VA...)" },
217
+ To: { type: "string", description: "Destination that received the code" },
218
+ Code: { type: "string", description: "Code the user entered" },
219
+ },
220
+ required: ["ServiceSid", "To", "Code"],
221
+ },
222
+ },
223
+ {
224
+ name: "lookup_phone",
225
+ description: "Validate and normalize a phone number via Lookups v2. Optional `Fields` lets you request carrier info, line_type_intelligence, caller_name, identity_match, etc.",
226
+ inputSchema: {
227
+ type: "object",
228
+ properties: {
229
+ PhoneNumber: { type: "string", description: "Number to look up (E.164 recommended; Lookups v2 will format if possible)" },
230
+ Fields: { type: "string", description: "Comma-separated list of add-ons (e.g. `line_type_intelligence,caller_name`). Billed per field." },
231
+ CountryCode: { type: "string", description: "ISO-3166 alpha-2. Required only if PhoneNumber is not in E.164." },
232
+ },
233
+ required: ["PhoneNumber"],
234
+ },
235
+ },
236
+ {
237
+ name: "list_incoming_numbers",
238
+ description: "List Twilio-provisioned phone numbers on this account. Filter by PhoneNumber (partial), FriendlyName, or Beta.",
239
+ inputSchema: {
240
+ type: "object",
241
+ properties: {
242
+ PhoneNumber: { type: "string", description: "Filter by partial phone number match" },
243
+ FriendlyName: { type: "string", description: "Filter by friendly name" },
244
+ PageSize: { type: "number", description: "Max rows per page (default 50, max 1000)" },
245
+ },
246
+ },
247
+ },
248
+ {
249
+ name: "buy_phone_number",
250
+ description: "Provision a new phone number. Supply either a specific `PhoneNumber` (from AvailablePhoneNumbers search) or an `AreaCode` to let Twilio pick one.",
251
+ inputSchema: {
252
+ type: "object",
253
+ properties: {
254
+ PhoneNumber: { type: "string", description: "Exact E.164 number to buy (from AvailablePhoneNumbers search)" },
255
+ AreaCode: { type: "string", description: "Area code — Twilio picks any available number in it" },
256
+ FriendlyName: { type: "string", description: "Friendly label" },
257
+ VoiceUrl: { type: "string", description: "TwiML URL for incoming calls" },
258
+ SmsUrl: { type: "string", description: "TwiML URL for incoming SMS" },
259
+ StatusCallback: { type: "string", description: "Webhook URL for number status events" },
260
+ },
261
+ },
262
+ },
263
+ ],
264
+ }));
265
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
266
+ const { name, arguments: args } = request.params;
267
+ const a = (args ?? {});
268
+ try {
269
+ switch (name) {
270
+ case "send_message": {
271
+ const body = {
272
+ To: a.To,
273
+ Body: a.Body,
274
+ };
275
+ if (a.From)
276
+ body.From = a.From;
277
+ else if (a.MessagingServiceSid)
278
+ body.MessagingServiceSid = a.MessagingServiceSid;
279
+ else if (DEFAULT_MESSAGING_SERVICE_SID)
280
+ body.MessagingServiceSid = DEFAULT_MESSAGING_SERVICE_SID;
281
+ if (a.MediaUrl)
282
+ body.MediaUrl = a.MediaUrl;
283
+ if (a.StatusCallback)
284
+ body.StatusCallback = a.StatusCallback;
285
+ return { content: [{ type: "text", text: JSON.stringify(await twilioRequest("POST", "/Messages.json", body), null, 2) }] };
286
+ }
287
+ case "get_message":
288
+ return { content: [{ type: "text", text: JSON.stringify(await twilioRequest("GET", `/Messages/${a.Sid}.json`), null, 2) }] };
289
+ case "list_messages": {
290
+ const q = buildQuery({
291
+ To: a.To,
292
+ From: a.From,
293
+ DateSent: a.DateSent,
294
+ "DateSent>": a.DateSentAfter,
295
+ "DateSent<": a.DateSentBefore,
296
+ PageSize: a.PageSize,
297
+ });
298
+ return { content: [{ type: "text", text: JSON.stringify(await twilioRequest("GET", `/Messages.json${q}`), null, 2) }] };
299
+ }
300
+ case "delete_message":
301
+ return { content: [{ type: "text", text: JSON.stringify(await twilioRequest("DELETE", `/Messages/${a.Sid}.json`), null, 2) }] };
302
+ case "make_call": {
303
+ const body = {
304
+ To: a.To,
305
+ From: a.From,
306
+ Url: a.Url,
307
+ };
308
+ if (a.Method)
309
+ body.Method = a.Method;
310
+ if (a.StatusCallback)
311
+ body.StatusCallback = a.StatusCallback;
312
+ if (a.StatusCallbackEvent)
313
+ body.StatusCallbackEvent = a.StatusCallbackEvent;
314
+ return { content: [{ type: "text", text: JSON.stringify(await twilioRequest("POST", "/Calls.json", body), null, 2) }] };
315
+ }
316
+ case "get_call":
317
+ return { content: [{ type: "text", text: JSON.stringify(await twilioRequest("GET", `/Calls/${a.Sid}.json`), null, 2) }] };
318
+ case "update_call": {
319
+ const body = {};
320
+ if (a.Status)
321
+ body.Status = a.Status;
322
+ if (a.Url)
323
+ body.Url = a.Url;
324
+ if (a.Method)
325
+ body.Method = a.Method;
326
+ return { content: [{ type: "text", text: JSON.stringify(await twilioRequest("POST", `/Calls/${a.Sid}.json`, body), null, 2) }] };
327
+ }
328
+ case "start_verification": {
329
+ const body = {
330
+ To: a.To,
331
+ Channel: a.Channel,
332
+ };
333
+ if (a.Locale)
334
+ body.Locale = a.Locale;
335
+ return { content: [{ type: "text", text: JSON.stringify(await twilioRequest("POST", `https://verify.twilio.com/v2/Services/${a.ServiceSid}/Verifications`, body), null, 2) }] };
336
+ }
337
+ case "check_verification": {
338
+ const body = {
339
+ To: a.To,
340
+ Code: a.Code,
341
+ };
342
+ return { content: [{ type: "text", text: JSON.stringify(await twilioRequest("POST", `https://verify.twilio.com/v2/Services/${a.ServiceSid}/VerificationCheck`, body), null, 2) }] };
343
+ }
344
+ case "lookup_phone": {
345
+ const q = buildQuery({ Fields: a.Fields, CountryCode: a.CountryCode });
346
+ const number = encodeURIComponent(String(a.PhoneNumber ?? ""));
347
+ return { content: [{ type: "text", text: JSON.stringify(await twilioRequest("GET", `https://lookups.twilio.com/v2/PhoneNumbers/${number}${q}`), null, 2) }] };
348
+ }
349
+ case "list_incoming_numbers": {
350
+ const q = buildQuery({
351
+ PhoneNumber: a.PhoneNumber,
352
+ FriendlyName: a.FriendlyName,
353
+ PageSize: a.PageSize,
354
+ });
355
+ return { content: [{ type: "text", text: JSON.stringify(await twilioRequest("GET", `/IncomingPhoneNumbers.json${q}`), null, 2) }] };
356
+ }
357
+ case "buy_phone_number": {
358
+ const body = {};
359
+ if (a.PhoneNumber)
360
+ body.PhoneNumber = a.PhoneNumber;
361
+ if (a.AreaCode)
362
+ body.AreaCode = a.AreaCode;
363
+ if (a.FriendlyName)
364
+ body.FriendlyName = a.FriendlyName;
365
+ if (a.VoiceUrl)
366
+ body.VoiceUrl = a.VoiceUrl;
367
+ if (a.SmsUrl)
368
+ body.SmsUrl = a.SmsUrl;
369
+ if (a.StatusCallback)
370
+ body.StatusCallback = a.StatusCallback;
371
+ return { content: [{ type: "text", text: JSON.stringify(await twilioRequest("POST", "/IncomingPhoneNumbers.json", body), null, 2) }] };
372
+ }
373
+ default:
374
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
375
+ }
376
+ }
377
+ catch (err) {
378
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
379
+ }
380
+ });
381
+ async function main() {
382
+ if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
383
+ const { default: express } = await import("express");
384
+ const { randomUUID } = await import("node:crypto");
385
+ const app = express();
386
+ app.use(express.json());
387
+ const transports = new Map();
388
+ app.get("/health", (_req, res) => res.json({ status: "ok", sessions: transports.size }));
389
+ app.post("/mcp", async (req, res) => {
390
+ const sid = req.headers["mcp-session-id"];
391
+ if (sid && transports.has(sid)) {
392
+ await transports.get(sid).handleRequest(req, res, req.body);
393
+ return;
394
+ }
395
+ if (!sid && isInitializeRequest(req.body)) {
396
+ const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
397
+ t.onclose = () => { if (t.sessionId)
398
+ transports.delete(t.sessionId); };
399
+ const s = new Server({ name: "mcp-twilio", version: "0.1.0" }, { capabilities: { tools: {} } });
400
+ server._requestHandlers.forEach((v, k) => s._requestHandlers.set(k, v));
401
+ server._notificationHandlers?.forEach((v, k) => s._notificationHandlers.set(k, v));
402
+ await s.connect(t);
403
+ await t.handleRequest(req, res, req.body);
404
+ return;
405
+ }
406
+ res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
407
+ });
408
+ app.get("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
409
+ await transports.get(sid).handleRequest(req, res);
410
+ else
411
+ res.status(400).send("Invalid session"); });
412
+ app.delete("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
413
+ await transports.get(sid).handleRequest(req, res);
414
+ else
415
+ res.status(400).send("Invalid session"); });
416
+ const port = Number(process.env.MCP_PORT) || 3000;
417
+ app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
418
+ }
419
+ else {
420
+ const transport = new StdioServerTransport();
421
+ await server.connect(transport);
422
+ }
423
+ }
424
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@codespar/mcp-twilio",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Twilio — global SMS, WhatsApp, Voice, Verify, and Lookup across 180+ countries",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "bin": {
8
+ "mcp-twilio": "./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
+ "twilio",
25
+ "sms",
26
+ "whatsapp",
27
+ "voice",
28
+ "verify",
29
+ "2fa",
30
+ "lookup",
31
+ "messaging",
32
+ "communication",
33
+ "global"
34
+ ],
35
+ "mcpName": "io.github.codespar/mcp-twilio"
36
+ }
package/server.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.codespar/mcp-twilio",
4
+ "description": "MCP server for Twilio — global SMS, WhatsApp, Voice, Verify, and Lookup across 180+ countries",
5
+ "repository": {
6
+ "url": "https://github.com/codespar/mcp-dev-brasil",
7
+ "source": "github",
8
+ "subfolder": "packages/communication/twilio"
9
+ },
10
+ "version": "0.1.0",
11
+ "packages": [
12
+ {
13
+ "registryType": "npm",
14
+ "identifier": "@codespar/mcp-twilio",
15
+ "version": "0.1.0",
16
+ "transport": {
17
+ "type": "stdio"
18
+ },
19
+ "environmentVariables": [
20
+ {
21
+ "name": "TWILIO_ACCOUNT_SID",
22
+ "description": "Twilio Account SID (starts with AC...). Used as username for HTTP Basic auth.",
23
+ "isRequired": true,
24
+ "format": "string",
25
+ "isSecret": false
26
+ },
27
+ {
28
+ "name": "TWILIO_AUTH_TOKEN",
29
+ "description": "Twilio Auth Token. Used as password for HTTP Basic auth.",
30
+ "isRequired": true,
31
+ "format": "string",
32
+ "isSecret": true
33
+ },
34
+ {
35
+ "name": "TWILIO_MESSAGING_SERVICE_SID",
36
+ "description": "Optional default Messaging Service SID (starts with MG...) used for send_message when From is omitted.",
37
+ "isRequired": false,
38
+ "format": "string",
39
+ "isSecret": false
40
+ }
41
+ ]
42
+ }
43
+ ]
44
+ }
package/src/index.ts ADDED
@@ -0,0 +1,413 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * MCP Server for Twilio — the global standard for programmable messaging + voice.
5
+ *
6
+ * One server covering Twilio's three most-used products:
7
+ * - Programmable Messaging (SMS + WhatsApp, same API, `whatsapp:` prefix)
8
+ * - Programmable Voice (outbound calls, hangup, redirect)
9
+ * - Verify (2FA one-time codes) + Lookups (phone validation)
10
+ *
11
+ * Fills the global-messaging gap in a catalog otherwise tilted to BR-specific
12
+ * providers (Z-API, Take Blip, Zenvia, Evolution API). Twilio ships in 180+
13
+ * countries on day one.
14
+ *
15
+ * Tools (12):
16
+ * send_message — send SMS or WhatsApp (prefix `To` with `whatsapp:+E164`)
17
+ * get_message — retrieve a message by SID
18
+ * list_messages — list messages with optional To/From/DateSent filters
19
+ * delete_message — delete a message from history
20
+ * make_call — place an outbound voice call driven by a TwiML Url
21
+ * get_call — retrieve a call by SID
22
+ * update_call — hang up or redirect an in-progress call
23
+ * start_verification — send a Verify (2FA) code via sms / whatsapp / call
24
+ * check_verification — check a Verify (2FA) code
25
+ * lookup_phone — validate + format + classify a phone number
26
+ * list_incoming_numbers — list provisioned Twilio phone numbers
27
+ * buy_phone_number — provision a new phone number
28
+ *
29
+ * Authentication
30
+ * HTTP Basic with AccountSid:AuthToken.
31
+ * Authorization: Basic <base64(AccountSid:AuthToken)>
32
+ *
33
+ * API surface
34
+ * Accounts API : https://api.twilio.com/2010-04-01/Accounts/{AccountSid}
35
+ * Verify API : https://verify.twilio.com/v2
36
+ * Lookups API : https://lookups.twilio.com/v2
37
+ *
38
+ * Request bodies are application/x-www-form-urlencoded. Responses are JSON
39
+ * (Accounts endpoints use the `.json` suffix).
40
+ *
41
+ * Environment
42
+ * TWILIO_ACCOUNT_SID required — Account SID (AC...)
43
+ * TWILIO_AUTH_TOKEN required — Auth Token (secret)
44
+ * TWILIO_MESSAGING_SERVICE_SID optional — default sender for send_message (MG...)
45
+ *
46
+ * Docs: https://www.twilio.com/docs/api
47
+ */
48
+
49
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
50
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
51
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
52
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
53
+ import {
54
+ CallToolRequestSchema,
55
+ ListToolsRequestSchema,
56
+ } from "@modelcontextprotocol/sdk/types.js";
57
+
58
+ const ACCOUNT_SID = process.env.TWILIO_ACCOUNT_SID || "";
59
+ const AUTH_TOKEN = process.env.TWILIO_AUTH_TOKEN || "";
60
+ const DEFAULT_MESSAGING_SERVICE_SID = process.env.TWILIO_MESSAGING_SERVICE_SID || "";
61
+ const ACCOUNTS_BASE = `https://api.twilio.com/2010-04-01/Accounts/${ACCOUNT_SID}`;
62
+
63
+ async function twilioRequest(
64
+ method: string,
65
+ fullUrlOrPath: string,
66
+ body?: Record<string, unknown>
67
+ ): Promise<unknown> {
68
+ const url = fullUrlOrPath.startsWith("https://")
69
+ ? fullUrlOrPath
70
+ : `${ACCOUNTS_BASE}${fullUrlOrPath}`;
71
+
72
+ const basic = Buffer.from(`${ACCOUNT_SID}:${AUTH_TOKEN}`).toString("base64");
73
+ const headers: Record<string, string> = {
74
+ "Authorization": `Basic ${basic}`,
75
+ "Accept": "application/json",
76
+ };
77
+
78
+ let encodedBody: string | undefined;
79
+ if (body && Object.keys(body).length > 0) {
80
+ const params = new URLSearchParams();
81
+ for (const [k, v] of Object.entries(body)) {
82
+ if (v === undefined || v === null) continue;
83
+ if (Array.isArray(v)) {
84
+ for (const item of v) params.append(k, String(item));
85
+ } else {
86
+ params.append(k, String(v));
87
+ }
88
+ }
89
+ encodedBody = params.toString();
90
+ headers["Content-Type"] = "application/x-www-form-urlencoded";
91
+ }
92
+
93
+ const res = await fetch(url, { method, headers, body: encodedBody });
94
+ if (!res.ok) {
95
+ throw new Error(`Twilio API ${res.status}: ${await res.text()}`);
96
+ }
97
+ const text = await res.text();
98
+ return text ? JSON.parse(text) : {};
99
+ }
100
+
101
+ function buildQuery(params: Record<string, unknown>): string {
102
+ const q = new URLSearchParams();
103
+ for (const [k, v] of Object.entries(params)) {
104
+ if (v === undefined || v === null || v === "") continue;
105
+ q.set(k, String(v));
106
+ }
107
+ const s = q.toString();
108
+ return s ? `?${s}` : "";
109
+ }
110
+
111
+ const server = new Server(
112
+ { name: "mcp-twilio", version: "0.1.0" },
113
+ { capabilities: { tools: {} } }
114
+ );
115
+
116
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
117
+ tools: [
118
+ {
119
+ name: "send_message",
120
+ description: "Send an SMS or WhatsApp message. For WhatsApp, prefix `To` (and `From`) with `whatsapp:+E164`. Supply either `From` (a Twilio phone number) OR `MessagingServiceSid` (a Messaging Service). If neither is given, falls back to env TWILIO_MESSAGING_SERVICE_SID.",
121
+ inputSchema: {
122
+ type: "object",
123
+ properties: {
124
+ To: { type: "string", description: "Destination in E.164 (e.g. +5511999999999) or `whatsapp:+E164` for WhatsApp" },
125
+ From: { type: "string", description: "Twilio phone number in E.164, or `whatsapp:+E164` for WhatsApp. Omit if using MessagingServiceSid." },
126
+ MessagingServiceSid: { type: "string", description: "Messaging Service SID (MG...). Overrides env default. Omit if using From." },
127
+ Body: { type: "string", description: "Message text (UTF-8)" },
128
+ MediaUrl: { type: "array", items: { type: "string" }, description: "Optional list of media URLs (MMS / WhatsApp media)" },
129
+ StatusCallback: { type: "string", description: "Webhook URL Twilio calls on delivery-status transitions" },
130
+ },
131
+ required: ["To", "Body"],
132
+ },
133
+ },
134
+ {
135
+ name: "get_message",
136
+ description: "Retrieve a message resource by SID (SM... or MM...).",
137
+ inputSchema: {
138
+ type: "object",
139
+ properties: {
140
+ Sid: { type: "string", description: "Message SID" },
141
+ },
142
+ required: ["Sid"],
143
+ },
144
+ },
145
+ {
146
+ name: "list_messages",
147
+ description: "List messages with optional filters. Returns Twilio's paginated list; pass PageSize to cap.",
148
+ inputSchema: {
149
+ type: "object",
150
+ properties: {
151
+ To: { type: "string", description: "Filter by destination (E.164 or whatsapp:+E164)" },
152
+ From: { type: "string", description: "Filter by sender" },
153
+ DateSent: { type: "string", description: "Filter by exact send date (YYYY-MM-DD). Use DateSentAfter / DateSentBefore for ranges." },
154
+ DateSentAfter: { type: "string", description: "Return messages sent on/after this date (YYYY-MM-DD)" },
155
+ DateSentBefore: { type: "string", description: "Return messages sent on/before this date (YYYY-MM-DD)" },
156
+ PageSize: { type: "number", description: "Max rows per page (default 50, max 1000)" },
157
+ },
158
+ },
159
+ },
160
+ {
161
+ name: "delete_message",
162
+ description: "Delete a message from history. Irreversible.",
163
+ inputSchema: {
164
+ type: "object",
165
+ properties: {
166
+ Sid: { type: "string", description: "Message SID" },
167
+ },
168
+ required: ["Sid"],
169
+ },
170
+ },
171
+ {
172
+ name: "make_call",
173
+ description: "Place an outbound voice call. Twilio fetches TwiML from `Url` on connect to drive the call.",
174
+ inputSchema: {
175
+ type: "object",
176
+ properties: {
177
+ To: { type: "string", description: "Destination number in E.164" },
178
+ From: { type: "string", description: "Twilio-provisioned caller ID in E.164" },
179
+ Url: { type: "string", description: "HTTP(S) URL returning TwiML that drives the call" },
180
+ Method: { type: "string", enum: ["GET", "POST"], description: "HTTP method Twilio uses to fetch Url (default POST)" },
181
+ StatusCallback: { type: "string", description: "Webhook URL for call-status events" },
182
+ StatusCallbackEvent: { type: "array", items: { type: "string" }, description: "Events to subscribe to: initiated, ringing, answered, completed" },
183
+ },
184
+ required: ["To", "From", "Url"],
185
+ },
186
+ },
187
+ {
188
+ name: "get_call",
189
+ description: "Retrieve a call resource by SID (CA...).",
190
+ inputSchema: {
191
+ type: "object",
192
+ properties: {
193
+ Sid: { type: "string", description: "Call SID" },
194
+ },
195
+ required: ["Sid"],
196
+ },
197
+ },
198
+ {
199
+ name: "update_call",
200
+ description: "Modify an in-progress call. Set Status='completed' to hang up, or pass a new Url to redirect the call to fresh TwiML.",
201
+ inputSchema: {
202
+ type: "object",
203
+ properties: {
204
+ Sid: { type: "string", description: "Call SID" },
205
+ Status: { type: "string", enum: ["canceled", "completed"], description: "canceled (before answered) or completed (hang up)" },
206
+ Url: { type: "string", description: "New TwiML URL — redirects the live call" },
207
+ Method: { type: "string", enum: ["GET", "POST"], description: "HTTP method for the new Url" },
208
+ },
209
+ required: ["Sid"],
210
+ },
211
+ },
212
+ {
213
+ name: "start_verification",
214
+ description: "Start a Verify (2FA) challenge. Sends a one-time code to `To` via the chosen channel. Requires a Verify Service SID (VA...).",
215
+ inputSchema: {
216
+ type: "object",
217
+ properties: {
218
+ ServiceSid: { type: "string", description: "Verify Service SID (VA...)" },
219
+ To: { type: "string", description: "Destination in E.164 (or email for email channel)" },
220
+ Channel: { type: "string", enum: ["sms", "whatsapp", "call", "email"], description: "Delivery channel" },
221
+ Locale: { type: "string", description: "Message locale (e.g. pt-br, en, es)" },
222
+ },
223
+ required: ["ServiceSid", "To", "Channel"],
224
+ },
225
+ },
226
+ {
227
+ name: "check_verification",
228
+ description: "Check a Verify (2FA) code against a Service SID. Returns status=approved when the code matches.",
229
+ inputSchema: {
230
+ type: "object",
231
+ properties: {
232
+ ServiceSid: { type: "string", description: "Verify Service SID (VA...)" },
233
+ To: { type: "string", description: "Destination that received the code" },
234
+ Code: { type: "string", description: "Code the user entered" },
235
+ },
236
+ required: ["ServiceSid", "To", "Code"],
237
+ },
238
+ },
239
+ {
240
+ name: "lookup_phone",
241
+ description: "Validate and normalize a phone number via Lookups v2. Optional `Fields` lets you request carrier info, line_type_intelligence, caller_name, identity_match, etc.",
242
+ inputSchema: {
243
+ type: "object",
244
+ properties: {
245
+ PhoneNumber: { type: "string", description: "Number to look up (E.164 recommended; Lookups v2 will format if possible)" },
246
+ Fields: { type: "string", description: "Comma-separated list of add-ons (e.g. `line_type_intelligence,caller_name`). Billed per field." },
247
+ CountryCode: { type: "string", description: "ISO-3166 alpha-2. Required only if PhoneNumber is not in E.164." },
248
+ },
249
+ required: ["PhoneNumber"],
250
+ },
251
+ },
252
+ {
253
+ name: "list_incoming_numbers",
254
+ description: "List Twilio-provisioned phone numbers on this account. Filter by PhoneNumber (partial), FriendlyName, or Beta.",
255
+ inputSchema: {
256
+ type: "object",
257
+ properties: {
258
+ PhoneNumber: { type: "string", description: "Filter by partial phone number match" },
259
+ FriendlyName: { type: "string", description: "Filter by friendly name" },
260
+ PageSize: { type: "number", description: "Max rows per page (default 50, max 1000)" },
261
+ },
262
+ },
263
+ },
264
+ {
265
+ name: "buy_phone_number",
266
+ description: "Provision a new phone number. Supply either a specific `PhoneNumber` (from AvailablePhoneNumbers search) or an `AreaCode` to let Twilio pick one.",
267
+ inputSchema: {
268
+ type: "object",
269
+ properties: {
270
+ PhoneNumber: { type: "string", description: "Exact E.164 number to buy (from AvailablePhoneNumbers search)" },
271
+ AreaCode: { type: "string", description: "Area code — Twilio picks any available number in it" },
272
+ FriendlyName: { type: "string", description: "Friendly label" },
273
+ VoiceUrl: { type: "string", description: "TwiML URL for incoming calls" },
274
+ SmsUrl: { type: "string", description: "TwiML URL for incoming SMS" },
275
+ StatusCallback: { type: "string", description: "Webhook URL for number status events" },
276
+ },
277
+ },
278
+ },
279
+ ],
280
+ }));
281
+
282
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
283
+ const { name, arguments: args } = request.params;
284
+ const a = (args ?? {}) as Record<string, unknown>;
285
+
286
+ try {
287
+ switch (name) {
288
+ case "send_message": {
289
+ const body: Record<string, unknown> = {
290
+ To: a.To,
291
+ Body: a.Body,
292
+ };
293
+ if (a.From) body.From = a.From;
294
+ else if (a.MessagingServiceSid) body.MessagingServiceSid = a.MessagingServiceSid;
295
+ else if (DEFAULT_MESSAGING_SERVICE_SID) body.MessagingServiceSid = DEFAULT_MESSAGING_SERVICE_SID;
296
+ if (a.MediaUrl) body.MediaUrl = a.MediaUrl;
297
+ if (a.StatusCallback) body.StatusCallback = a.StatusCallback;
298
+ return { content: [{ type: "text", text: JSON.stringify(await twilioRequest("POST", "/Messages.json", body), null, 2) }] };
299
+ }
300
+ case "get_message":
301
+ return { content: [{ type: "text", text: JSON.stringify(await twilioRequest("GET", `/Messages/${a.Sid}.json`), null, 2) }] };
302
+ case "list_messages": {
303
+ const q = buildQuery({
304
+ To: a.To,
305
+ From: a.From,
306
+ DateSent: a.DateSent,
307
+ "DateSent>": a.DateSentAfter,
308
+ "DateSent<": a.DateSentBefore,
309
+ PageSize: a.PageSize,
310
+ });
311
+ return { content: [{ type: "text", text: JSON.stringify(await twilioRequest("GET", `/Messages.json${q}`), null, 2) }] };
312
+ }
313
+ case "delete_message":
314
+ return { content: [{ type: "text", text: JSON.stringify(await twilioRequest("DELETE", `/Messages/${a.Sid}.json`), null, 2) }] };
315
+ case "make_call": {
316
+ const body: Record<string, unknown> = {
317
+ To: a.To,
318
+ From: a.From,
319
+ Url: a.Url,
320
+ };
321
+ if (a.Method) body.Method = a.Method;
322
+ if (a.StatusCallback) body.StatusCallback = a.StatusCallback;
323
+ if (a.StatusCallbackEvent) body.StatusCallbackEvent = a.StatusCallbackEvent;
324
+ return { content: [{ type: "text", text: JSON.stringify(await twilioRequest("POST", "/Calls.json", body), null, 2) }] };
325
+ }
326
+ case "get_call":
327
+ return { content: [{ type: "text", text: JSON.stringify(await twilioRequest("GET", `/Calls/${a.Sid}.json`), null, 2) }] };
328
+ case "update_call": {
329
+ const body: Record<string, unknown> = {};
330
+ if (a.Status) body.Status = a.Status;
331
+ if (a.Url) body.Url = a.Url;
332
+ if (a.Method) body.Method = a.Method;
333
+ return { content: [{ type: "text", text: JSON.stringify(await twilioRequest("POST", `/Calls/${a.Sid}.json`, body), null, 2) }] };
334
+ }
335
+ case "start_verification": {
336
+ const body: Record<string, unknown> = {
337
+ To: a.To,
338
+ Channel: a.Channel,
339
+ };
340
+ if (a.Locale) body.Locale = a.Locale;
341
+ return { content: [{ type: "text", text: JSON.stringify(await twilioRequest("POST", `https://verify.twilio.com/v2/Services/${a.ServiceSid}/Verifications`, body), null, 2) }] };
342
+ }
343
+ case "check_verification": {
344
+ const body: Record<string, unknown> = {
345
+ To: a.To,
346
+ Code: a.Code,
347
+ };
348
+ return { content: [{ type: "text", text: JSON.stringify(await twilioRequest("POST", `https://verify.twilio.com/v2/Services/${a.ServiceSid}/VerificationCheck`, body), null, 2) }] };
349
+ }
350
+ case "lookup_phone": {
351
+ const q = buildQuery({ Fields: a.Fields, CountryCode: a.CountryCode });
352
+ const number = encodeURIComponent(String(a.PhoneNumber ?? ""));
353
+ return { content: [{ type: "text", text: JSON.stringify(await twilioRequest("GET", `https://lookups.twilio.com/v2/PhoneNumbers/${number}${q}`), null, 2) }] };
354
+ }
355
+ case "list_incoming_numbers": {
356
+ const q = buildQuery({
357
+ PhoneNumber: a.PhoneNumber,
358
+ FriendlyName: a.FriendlyName,
359
+ PageSize: a.PageSize,
360
+ });
361
+ return { content: [{ type: "text", text: JSON.stringify(await twilioRequest("GET", `/IncomingPhoneNumbers.json${q}`), null, 2) }] };
362
+ }
363
+ case "buy_phone_number": {
364
+ const body: Record<string, unknown> = {};
365
+ if (a.PhoneNumber) body.PhoneNumber = a.PhoneNumber;
366
+ if (a.AreaCode) body.AreaCode = a.AreaCode;
367
+ if (a.FriendlyName) body.FriendlyName = a.FriendlyName;
368
+ if (a.VoiceUrl) body.VoiceUrl = a.VoiceUrl;
369
+ if (a.SmsUrl) body.SmsUrl = a.SmsUrl;
370
+ if (a.StatusCallback) body.StatusCallback = a.StatusCallback;
371
+ return { content: [{ type: "text", text: JSON.stringify(await twilioRequest("POST", "/IncomingPhoneNumbers.json", body), null, 2) }] };
372
+ }
373
+ default:
374
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
375
+ }
376
+ } catch (err) {
377
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
378
+ }
379
+ });
380
+
381
+ async function main() {
382
+ if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
383
+ const { default: express } = await import("express");
384
+ const { randomUUID } = await import("node:crypto");
385
+ const app = express();
386
+ app.use(express.json());
387
+ const transports = new Map<string, StreamableHTTPServerTransport>();
388
+ app.get("/health", (_req: unknown, res: { json: (body: unknown) => unknown }) => res.json({ status: "ok", sessions: transports.size }));
389
+ app.post("/mcp", async (req: { headers: Record<string, string | string[] | undefined>; body: unknown }, res: { status: (code: number) => { json: (body: unknown) => unknown } }) => {
390
+ const sid = req.headers["mcp-session-id"] as string | undefined;
391
+ if (sid && transports.has(sid)) { await transports.get(sid)!.handleRequest(req as never, res as never, req.body); return; }
392
+ if (!sid && isInitializeRequest(req.body)) {
393
+ const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
394
+ t.onclose = () => { if (t.sessionId) transports.delete(t.sessionId); };
395
+ const s = new Server({ name: "mcp-twilio", version: "0.1.0" }, { capabilities: { tools: {} } });
396
+ (server as unknown as { _requestHandlers: Map<unknown, unknown> })._requestHandlers.forEach((v, k) => (s as unknown as { _requestHandlers: Map<unknown, unknown> })._requestHandlers.set(k, v));
397
+ (server as unknown as { _notificationHandlers?: Map<unknown, unknown> })._notificationHandlers?.forEach((v, k) => (s as unknown as { _notificationHandlers: Map<unknown, unknown> })._notificationHandlers.set(k, v));
398
+ await s.connect(t);
399
+ await t.handleRequest(req as never, res as never, req.body); return;
400
+ }
401
+ res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
402
+ });
403
+ 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"); });
404
+ 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"); });
405
+ const port = Number(process.env.MCP_PORT) || 3000;
406
+ app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
407
+ } else {
408
+ const transport = new StdioServerTransport();
409
+ await server.connect(transport);
410
+ }
411
+ }
412
+
413
+ 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
+ }